닫기
일을 마친 세션을 닫고, 멈춰야 하는 실행을 terminate하고, 결과를 알 수 없는 turn을 복구한다
세션을 끝내는 길은 셋이다. 일을 마친 세션은 close로 닫는다. 지금 당장 멈춰야 하면 terminate한다. terminate로 결과를 알 수 없는 turn이 남으면 복구 결정을 한다. 시작에서 만든 세션(SDK는 sessionId, HTTP API는 SID)을 이어 쓴다.
| close | terminate | 복구 결정 close | |
|---|---|---|---|
| SDK | sessions.close | sessions.terminate | sessions.recoveryDecisions.create |
| HTTP API | POST …/close | POST …/terminate | POST …/recovery-decisions |
| scope | sessions:control | sessions:control | sessions:recover |
| 진행 중 turn | 끝까지 돌리고 checkpoint를 남긴다 | 실행을 죽인다. turn이 outcome_unknown이 될 수 있다 | 이미 멈춘 세션이 대상이다 |
| 대기 중인 입력 | 닫힐 때 취소 | 바로 취소 | 바로 취소 |
| 결과 | closed(되돌릴 수 없음) | stopped(resume 가능) | closed(되돌릴 수 없음) |
close
close는 돌고 있는 turn을 끝까지 기다린다. 아직 시작하지 않은 turn은 취소된다(cancelled_turn_count). 아래는 첫 turn이 끝난 뒤에 닫는다.
import { closeResult, waitForReceipt, waitForTurn } from "@werft-ai/sdk";
await waitForTurn(werft, sessionId, created.turn_id);
const { revision } = await werft.sessions.get(sessionId);
const accepted = await werft.sessions.close(sessionId, {
expected_revision: revision,
reason: "work is done",
});
const receipt = await waitForReceipt(werft, accepted.receipt_id);
console.log(receipt.operation, receipt.status, closeResult(receipt));close succeeded {
checkpoint_revision: 0,
cancelled_turn_count: 0,
resulting_admission_state: 'closed'
}closeResult는 close receipt의 result를 타입으로 꺼낸다. 열린 요청 때문에 막혔는지는 closeBlocked(session)으로 본다.
닫는 동안 세션은 closing이다. 열린 권한 요청이나 질문은 close를 막는다. 답하거나 interrupt하면 풀린다. 60초가 넘으면 attention이 CLOSE_BLOCKED가 된다. 닫힌 뒤의 메시지와 pause·resume·close·terminate는 409 SESSION_CLOSED다. 이미 끝난 turn을 가리키는 interrupt는 닫힌 뒤에도 아무것도 하지 않는 성공(no_op: true)이다.
terminate
terminate는 실행을 바로 내린다. 도구가 이미 바깥에 한 일은 되돌리지 않는다. 쉬고 있는 세션을 terminate하면 바로 stopped이고 그대로 resume할 수 있다. 돌고 있던 turn이 있으면 그 turn은 outcome_unknown이 되고 세션은 복구 결정을 기다린다.
const runaway = await werft.sessions.create({
profile_id: "claude-coding-local",
repository_id: "sample-app",
message:
'GATE-SPEC {"id":"runaway","steps":[],"final":"never","finalDelayMs":300000}',
});
const id = runaway.session_id;
while ((await werft.sessions.turns.get(id, runaway.turn_id)).status !== "running") {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
const current = await werft.sessions.get(id);
const terminated = await werft.sessions.terminate(id, {
expected_revision: current.revision,
reason: "runaway turn",
});
console.log(terminated.receipt_status, terminated.external_effects_reverted);
while ((await werft.sessions.get(id)).admission_state !== "recovery_required") {
await new Promise((resolve) => setTimeout(resolve, 2000));
}
console.log((await werft.sessions.turns.get(id, runaway.turn_id)).status);accepted false
outcome_unknown복구 결정
outcome_unknown은 성공도 실패도 아니다. 그 turn이 바깥에 무엇을 했는지 확인한 사람이 결정한다.
| 결정 | 하는 일 |
|---|---|
abandon | 그 turn을 cancelled로 두고 마지막 checkpoint에서 이어 갈 수 있게 한다 |
confirm_completed | checkpoint가 그 turn까지 덮을 때, 끝난 것으로 인정한다. evidence_ref가 필요하다 |
start_fresh | 이전 대화를 버리고 새 Claude Code 세션에서 대기 중인 입력을 실행한다 |
retry_restore | checkpoint 복원 실패의 원인을 고친 뒤 같은 checkpoint로 다시 복원한다 |
close | 세션을 끝낸다. 되돌릴 수 없다 |
const stopped = await werft.sessions.get(id);
const decided = await werft.sessions.recoveryDecisions.create(id, {
decision: "close",
expected_revision: stopped.revision,
reason: "nothing to keep",
});
const decision = await waitForReceipt(werft, decided.receipt_id);
console.log(decision.operation, decision.result);recovery_decision {
resumable: false,
checkpoint_revision: null,
resulting_admission_state: 'closed'
}key에 sessions:recover가 있어야 한다.
abandon과 confirm_completed에는 대상 turn을 target_turn_id로 함께 보낸다. 다른 결정은 받지 않는다(400). abandon의 receipt result.resumable이 true면 resume으로 이어 간다.
다음 단계
- receipt: 제어 명령의 결과 읽기
- 저장소 없는 세션
session-close.ts샘플: 세션 생성, 이벤트를 받으며 turn 대기, 권한 요청 허락, close, 닫힌 뒤의 메시지 거절(409SESSION_CLOSED)까지 한 번에 도는 SDK 스크립트