aiterm-mcp 0.61.7 → 0.61.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/README.ja.md +8 -2
- package/README.md +8 -2
- package/dist/core.js +123 -97
- package/dist/execution-close.js +1 -5
- package/dist/execution-contract.js +4 -0
- package/dist/execution-dispatch.js +7 -0
- package/dist/execution-launch-recovery.js +3 -0
- package/dist/execution-native-server.js +1 -0
- package/dist/execution-parent-producer.js +12 -2
- package/dist/execution-participant.js +1 -1
- package/dist/execution-runtime.js +4 -2
- package/dist/execution-service.js +129 -18
- package/dist/execution-tools.js +12 -4
- package/dist/execution-wire.js +18 -2
- package/docs/DESIGN.md +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.9] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Asynchronous agent launches wait for graph admission before their initial checks and their synchronous create/metadata/CLI input segment. CLI validation and readiness waits stay outside that mutex. A short competing writer cannot strand a managed shell between generation binding and CLI input; native effects are never retried.
|
|
15
|
+
- Explicit native `pty_close` can recover a returned unknown launch only when the original native/server and managed generation still match, the raw initial prompt is absent, send remains unissued and the harness is confirmed exited. The close keeps the failed record and releases its slot only after native close and confirmed absence. Issued RPCs, live harnesses and uncertain input or identity remain blocked.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- The BellTeam native execution entry accepts explicit `agent_execution/close_launch` with the original request and exact target generation. It checks that expectation during admission and before close, uses the serving SDK management identity, and returns a versioned `_meta["aiterm/closed_launch"]` receipt only after close, confirmed absence and metadata commit. The original unknown record stays unchanged. Failed or uncertain closes return no closed receipt; later generations and historical receipts cannot authorize another close. The proof supports empty exited Claude Code and Codex launches.
|
|
20
|
+
|
|
21
|
+
## [0.61.8] - 2026-10-23
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Repeating empty-launch reconciliation now rechecks the current managed generation, no-input readiness and competing session operations before returning an accepted launch with an unissued send. A historical accepted receipt cannot authorize clearing a newer generation's launch uncertainty. Rejection preserves the existing graph and issues no native input.
|
|
26
|
+
|
|
10
27
|
## [0.61.7] - 2026-10-23
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -2753,7 +2770,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2753
2770
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2754
2771
|
provenance.
|
|
2755
2772
|
|
|
2756
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2773
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.9...HEAD
|
|
2774
|
+
[0.61.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...v0.61.9
|
|
2775
|
+
[0.61.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...v0.61.8
|
|
2757
2776
|
[0.61.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...v0.61.7
|
|
2758
2777
|
[0.61.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...v0.61.6
|
|
2759
2778
|
[0.61.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...v0.61.5
|
package/README.ja.md
CHANGED
|
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
209
209
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
210
210
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
211
211
|
|
|
212
|
-
**状態:** 開発継続中 · 現行公開版 **v0.61.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.61.9** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -738,7 +738,13 @@ BellTeamのmodel・effort・fast変更も、既存session世代に結合したna
|
|
|
738
738
|
|
|
739
739
|
POSIXの明示native入口でも、`agent_auth`の名前なし開始、認証済みで何も起こさない開始、既存v1の確認・初回案内・取消を受け付ける。認証PTYは実際の作成元nativeと世代へ結合する。BellTeamの参照なし入力・キーは、この証拠がある認証PTYにだけ許可し、通常agentの本文には配送参照を引き続き必要とする。作成証拠のない旧認証metadataを自動登録しない。
|
|
740
740
|
|
|
741
|
-
この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch
|
|
741
|
+
この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch枠だけを解放する。受付済みでも本文未発行なら、照合を繰り返すたびに現在世代・入力待ち・競合操作なしを再確認する。保存した受付結果だけで後の世代を確認済みとしない。起動・打鍵・close・本文再送は行わない。issuedのRPC、入力状態不明、世代交代や世代未確認は拒否する。後続本文は元参照で通常のsend受付を通して一度だけ発行する。照合成功は失われた元RPC receiptの回収を意味しない。
|
|
742
|
+
|
|
743
|
+
CLIが終了した結果不明の空起動には、BellTeam native入口の `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})` を明示的に使う。元requestと期待世代を受付内とnative close直前で照合し、原native/server・managed世代一致、raw初期prompt `none`・初期配送なし・send未発行・現在CLI終了の正規probeがすべて必要になる。Claude CodeとCodexで同じ証明条件を使う。生存・issued・送信済み・欠測・世代交代は拒否し、sessionだけのcloseへの代用や自動再起動・再送はしない。旧版は新actionを拒否する。
|
|
744
|
+
|
|
745
|
+
成功時の `structuredContent` は応答するSDKの別管理requestのExecutionRecordで、元unknown recordは変えない。`_meta["aiterm/closed_launch"]` に厳密なreceiptを返す。固定項目は `schema: "aiterm.closed-launch.v1"`、`version: 1`、`action: "close_launch"`、`outcome: "closed"`、元 `request`、正確な `target`、`management_ref`(応答SDKのMCP request、`tool: "pty_close"`)。`evidence` は原 `native` binding、`initial_prompt: "none"`、`initial_prompt_delivery: null`、`harness_alive: false`、`absence_confirmed: true`、`metadata_committed: true` を持つ。実close・不在確認・slot解放commitが揃った場合だけ返し、失敗や結果不明には返さない。本体が不明を解除する際は元pending操作と自身のstop/versionも照合する。過去receiptで後発の不明を解除しない。
|
|
746
|
+
|
|
747
|
+
同じ明示入口で、返却済みunknown launchのCLIが終了している場合は、元native/server・managed世代とraw初期promptなし・本文未発行・harness終了を同期検証できた時だけ通常の`pty_close`で対象席を閉じられる。成功と不在を確認して該当枠だけを外し、元unknown requestを保持する。発行中RPC、生存harness、入力や世代の欠測は拒否する。旧requestを再実行せず、必要な後続は新しい通常要求として行う。
|
|
742
748
|
|
|
743
749
|
|
|
744
750
|
## 人が覗く
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
227
227
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
228
228
|
Release re-registers the Official MCP Registry entry.
|
|
229
229
|
|
|
230
|
-
**Status:** actively maintained · current public release **v0.61.
|
|
230
|
+
**Status:** actively maintained · current public release **v0.61.9** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
231
231
|
|
|
232
232
|
### Update and rollback
|
|
233
233
|
|
|
@@ -796,7 +796,13 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
|
|
|
796
796
|
|
|
797
797
|
In the opt-in POSIX native execution entry, an ordinary message arriving at a verified caller can overlap that caller's outgoing send. It does not replace the caller's generation or clear either request's slot. Agent paste/submit segments wait for metadata mutex contention before input; uncertain operations, generation changes and stops still prevent input. BellTeam may explicitly recreate a closed session with a promptless management `agent_launch`; sending a prompt still requires its original execution reference.
|
|
798
798
|
|
|
799
|
-
An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
|
|
799
|
+
An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. Repeating an accepted empty launch before its send also rechecks the current generation, readiness and absence of competing session operations; a saved acceptance alone cannot confirm a later generation. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
|
|
800
|
+
|
|
801
|
+
For an empty unknown launch whose harness has exited, the BellTeam native entry accepts `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})`. The original request and generation are atomic expectations, checked within admission and immediately before the native close. Raw initial prompt must be `none`, initial delivery absent, send unissued, and the original native/server and managed identity must match. The private runtime proof covers Claude Code and Codex; live, issued, sent, missing or replaced targets remain refused. No session-only close fallback or automatic relaunch/send occurs. Older servers reject this action.
|
|
802
|
+
|
|
803
|
+
Success retains the original unknown record and returns the serving SDK's distinct management execution record in `structuredContent`. `_meta["aiterm/closed_launch"]` is a strict receipt with `schema: "aiterm.closed-launch.v1"`, `version: 1`, `action: "close_launch"`, `outcome: "closed"`, the original `request`, exact `target`, and `management_ref` (the serving MCP request, `tool: "pty_close"`). Its `evidence` contains original `native` binding, `initial_prompt: "none"`, `initial_prompt_delivery: null`, `harness_alive: false`, `absence_confirmed: true`, and `metadata_committed: true`. Only confirmed close, absence and committed slot release produce this receipt. Errors or lost/uncertain results do not. Hosts must match the original pending operation and local stop/version state before clearing its uncertainty; historical receipts cannot clear later uncertainty.
|
|
804
|
+
|
|
805
|
+
For a returned unknown launch whose harness has exited, the same native entry admits explicit `pty_close` only after synchronous proof of the original native/server and managed generation, raw no-initial-prompt state, an unissued body send and an exited harness. It releases the failed launch slot after successful close and confirmed absence, preserving the original unknown record. Issued RPCs, live harnesses and missing input or generation evidence remain blocked. The old request is never replayed; a later ordinary request may create a fresh generation.
|
|
800
806
|
|
|
801
807
|
|
|
802
808
|
Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on native Windows. The attach line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene, including a Claude/Codex/Grok/Cursor harness session: `tmux -S … attach -t <id>` on POSIX, or `psmux -L <namespace> attach -t <id>` on native Windows.
|
package/dist/core.js
CHANGED
|
@@ -1701,6 +1701,14 @@ export function selectExecutionAuthSessionName() { return "auth-" + randomBytes(
|
|
|
1701
1701
|
/** Read-only proof for reconciling a created, empty launch. Inspect raw prompt
|
|
1702
1702
|
* state: legacy/default normalization must never prove that input was absent. */
|
|
1703
1703
|
export function observeExecutionCreatedLaunch(target) {
|
|
1704
|
+
return observeEmptyCreatedLaunch(target, true);
|
|
1705
|
+
}
|
|
1706
|
+
/** Explicit management close only. No generation enrollment or acceptance of
|
|
1707
|
+
* the failed launch; an unknown/missing harness proof remains a refusal. */
|
|
1708
|
+
export function observeExecutionExitedCreatedLaunch(target) {
|
|
1709
|
+
return observeEmptyCreatedLaunch(target, false);
|
|
1710
|
+
}
|
|
1711
|
+
function observeEmptyCreatedLaunch(target, alive) {
|
|
1704
1712
|
target = executionLaunchSchema.parse(target);
|
|
1705
1713
|
const unknown = () => { throw new ExecutionError("EXECUTION_UNCONFIRMED"); };
|
|
1706
1714
|
const generation = () => {
|
|
@@ -1724,12 +1732,14 @@ export function observeExecutionCreatedLaunch(target) {
|
|
|
1724
1732
|
};
|
|
1725
1733
|
generation();
|
|
1726
1734
|
const native = read(), live = observeSession(target.session_id);
|
|
1727
|
-
if (live.launch_id !== target.launch_id || live.harness_alive !==
|
|
1735
|
+
if (live.launch_id !== target.launch_id || live.harness_alive !== alive
|
|
1736
|
+
|| (alive ? live.harness_process === null || live.state !== "idle"
|
|
1737
|
+
: live.harness_process !== null || live.state !== "dead" || live.reason !== "harness_exited"))
|
|
1728
1738
|
unknown();
|
|
1729
1739
|
generation();
|
|
1730
1740
|
if (!isDeepStrictEqual(read(), native))
|
|
1731
1741
|
unknown();
|
|
1732
|
-
return { target, native, initial_prompt: "none", initial_prompt_delivery: null, harness_alive:
|
|
1742
|
+
return { target, native, initial_prompt: "none", initial_prompt_delivery: null, harness_alive: alive, ready: alive };
|
|
1733
1743
|
}
|
|
1734
1744
|
/** Private source-provenance snapshot. It deliberately omits screen, argv,
|
|
1735
1745
|
* environment and cached vendor_session_id. No public tool is added. The
|
|
@@ -5044,7 +5054,7 @@ async function launchKeepingUserDefaults(configPath, o, launch) {
|
|
|
5044
5054
|
return withUserConfigLock(configPath, async () => {
|
|
5045
5055
|
const before = readFileOrNull(configPath);
|
|
5046
5056
|
const mustWrite = o.mustWrite(before);
|
|
5047
|
-
const launched = launch();
|
|
5057
|
+
const launched = await launch();
|
|
5048
5058
|
const started = performance.now();
|
|
5049
5059
|
const deadline = started + (mustWrite ? LAUNCH_DEFAULT_MUST_WRITE_WAIT_MS : LAUNCH_DEFAULT_WAIT_MS);
|
|
5050
5060
|
let aliveCheckedAt = started;
|
|
@@ -6366,6 +6376,13 @@ function existingAgentLaunchResult(kind, sid, model, effort) {
|
|
|
6366
6376
|
}
|
|
6367
6377
|
export function openAgent(kind, opts = {}) {
|
|
6368
6378
|
assertExecutionContext(opts.session_name);
|
|
6379
|
+
return prepareAgentLaunch(kind, opts)();
|
|
6380
|
+
}
|
|
6381
|
+
/** Resolve and validate the CLI before taking native admission. Only the
|
|
6382
|
+
* returned synchronous creation/metadata/CLI segment shares the graph mutex.
|
|
6383
|
+
* Public synchronous openAgent retains its existing immediate refusal contract.
|
|
6384
|
+
*/
|
|
6385
|
+
function prepareAgentLaunch(kind, opts = {}) {
|
|
6369
6386
|
const label = agentLabel(kind);
|
|
6370
6387
|
// 前提検証は session を作る前に全部済ませる(失敗の残骸 session を作らない)。
|
|
6371
6388
|
// model/effort → bin → cwd の順: model/effort 検証は CLI 不在の端末でも同じ結果になる(テスト可能性)。
|
|
@@ -6459,7 +6476,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
6459
6476
|
});
|
|
6460
6477
|
if (launchOperationId !== null && sessionExists(opts.session_name)) {
|
|
6461
6478
|
requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
|
|
6462
|
-
return existingAgentLaunchResult("claude", opts.session_name, model, effort);
|
|
6479
|
+
return () => existingAgentLaunchResult("claude", opts.session_name, model, effort);
|
|
6463
6480
|
}
|
|
6464
6481
|
if (kind === "claude")
|
|
6465
6482
|
assertClaudeAuthenticationReady(bin);
|
|
@@ -6489,100 +6506,102 @@ export function openAgent(kind, opts = {}) {
|
|
|
6489
6506
|
if (kind === "cursor" && model) {
|
|
6490
6507
|
assertCursorModelAvailable(bin, cwd ?? process.cwd(), model, effort, cursorFast);
|
|
6491
6508
|
}
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
catch (error) {
|
|
6500
|
-
if (launchOperationId !== null && sessionExists(opts.session_name)) {
|
|
6501
|
-
requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
|
|
6502
|
-
return existingAgentLaunchResult("claude", opts.session_name, model, effort);
|
|
6509
|
+
return () => {
|
|
6510
|
+
let sid;
|
|
6511
|
+
let hint;
|
|
6512
|
+
// WindowsのCursorだけはPowerShell 7 paneから起動する(理由はcursorPwshLaunchLine)。
|
|
6513
|
+
const paneShell = isWin && kind === "cursor" && agentDone ? "pwsh" : "bash";
|
|
6514
|
+
try {
|
|
6515
|
+
[sid, hint] = openSession(opts.session_name ?? null, paneShell, envVars);
|
|
6503
6516
|
}
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
const lineageContext = createAgentLineageContext(kind, sid, lineageSeed);
|
|
6509
|
-
if (hasExecutionCreationScope())
|
|
6510
|
-
beforeExecutionEffect(sid);
|
|
6511
|
-
const meta = withExecutionPreparation(sid, () => {
|
|
6512
|
-
const meta = agentDone
|
|
6513
|
-
? kind === "claude"
|
|
6514
|
-
? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
|
|
6515
|
-
: kind === "codex"
|
|
6516
|
-
? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
|
|
6517
|
-
: kind === "cursor"
|
|
6518
|
-
? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
|
|
6519
|
-
: createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
|
|
6520
|
-
: null;
|
|
6521
|
-
if (meta) {
|
|
6522
|
-
// Metadata constructors publish their internally generated launch ID.
|
|
6523
|
-
// Stamp its exact native creator before binding and any later input.
|
|
6524
|
-
if (hasExecutionCreationScope())
|
|
6525
|
-
meta.execution_native = executionCreatedNativeBinding(sid);
|
|
6526
|
-
meta.agent_executable = bin;
|
|
6527
|
-
// 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
|
|
6528
|
-
meta.fast = opts.fast_state === true;
|
|
6529
|
-
meta.model = opts.base_model !== undefined ? opts.base_model : model;
|
|
6530
|
-
// The new physical shell is already pinned by its actual create receipt.
|
|
6531
|
-
// Validate it before publishing metadata, then bind this internally
|
|
6532
|
-
// generated managed launch. Model arguments cannot select this ID.
|
|
6533
|
-
writeAgentMetadata(meta);
|
|
6534
|
-
agentMetadataNegativeCache.delete(sid);
|
|
6517
|
+
catch (error) {
|
|
6518
|
+
if (launchOperationId !== null && sessionExists(opts.session_name)) {
|
|
6519
|
+
requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
|
|
6520
|
+
return existingAgentLaunchResult("claude", opts.session_name, model, effort);
|
|
6535
6521
|
}
|
|
6536
|
-
|
|
6537
|
-
});
|
|
6538
|
-
if (meta && hasExecutionCreationScope())
|
|
6539
|
-
bindExecutionCreatedLaunch({ session_id: sid, launch_id: meta.launch_id });
|
|
6540
|
-
launchNote = buildAgentLaunchNote(kind, model, effort, meta);
|
|
6541
|
-
let full;
|
|
6542
|
-
if (paneShell === "pwsh" && meta) {
|
|
6543
|
-
full = cursorPwshLaunchLine(cwd, cursorAgentEnvEntries(meta, sid, envVars), cursorPwshArgv(cursorAgentArgv(bin, model, effort, opts.prompt ?? null, meta, cursorFast), () => resolveWinPaneShell("bash")));
|
|
6522
|
+
throw error;
|
|
6544
6523
|
}
|
|
6545
|
-
|
|
6546
|
-
const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta, cursorFast);
|
|
6547
|
-
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
6548
|
-
full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
6549
|
-
}
|
|
6550
|
-
if (meta)
|
|
6551
|
-
agentLaunchLines.set(sid, full);
|
|
6552
|
-
// force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
|
|
6553
|
-
send(sid, full, {
|
|
6554
|
-
enter: true,
|
|
6555
|
-
mark: false,
|
|
6556
|
-
force: true,
|
|
6557
|
-
rtk: false,
|
|
6558
|
-
raw: true,
|
|
6559
|
-
preserveAgentOperation: meta?.kind === "claude",
|
|
6560
|
-
});
|
|
6561
|
-
}
|
|
6562
|
-
catch (e) {
|
|
6563
|
-
const failure = e instanceof ExecutionError ? e : telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
|
|
6564
|
-
// 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
|
|
6524
|
+
let launchNote = "";
|
|
6565
6525
|
try {
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6526
|
+
const lineageContext = createAgentLineageContext(kind, sid, lineageSeed);
|
|
6527
|
+
if (hasExecutionCreationScope())
|
|
6528
|
+
beforeExecutionEffect(sid);
|
|
6529
|
+
const meta = withExecutionPreparation(sid, () => {
|
|
6530
|
+
const meta = agentDone
|
|
6531
|
+
? kind === "claude"
|
|
6532
|
+
? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
|
|
6533
|
+
: kind === "codex"
|
|
6534
|
+
? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
|
|
6535
|
+
: kind === "cursor"
|
|
6536
|
+
? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
|
|
6537
|
+
: createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
|
|
6538
|
+
: null;
|
|
6539
|
+
if (meta) {
|
|
6540
|
+
// Metadata constructors publish their internally generated launch ID.
|
|
6541
|
+
// Stamp its exact native creator before binding and any later input.
|
|
6542
|
+
if (hasExecutionCreationScope())
|
|
6543
|
+
meta.execution_native = executionCreatedNativeBinding(sid);
|
|
6544
|
+
meta.agent_executable = bin;
|
|
6545
|
+
// 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
|
|
6546
|
+
meta.fast = opts.fast_state === true;
|
|
6547
|
+
meta.model = opts.base_model !== undefined ? opts.base_model : model;
|
|
6548
|
+
// The new physical shell is already pinned by its actual create receipt.
|
|
6549
|
+
// Validate it before publishing metadata, then bind this internally
|
|
6550
|
+
// generated managed launch. Model arguments cannot select this ID.
|
|
6551
|
+
writeAgentMetadata(meta);
|
|
6552
|
+
agentMetadataNegativeCache.delete(sid);
|
|
6553
|
+
}
|
|
6554
|
+
return meta;
|
|
6555
|
+
});
|
|
6556
|
+
if (meta && hasExecutionCreationScope())
|
|
6557
|
+
bindExecutionCreatedLaunch({ session_id: sid, launch_id: meta.launch_id });
|
|
6558
|
+
launchNote = buildAgentLaunchNote(kind, model, effort, meta);
|
|
6559
|
+
let full;
|
|
6560
|
+
if (paneShell === "pwsh" && meta) {
|
|
6561
|
+
full = cursorPwshLaunchLine(cwd, cursorAgentEnvEntries(meta, sid, envVars), cursorPwshArgv(cursorAgentArgv(bin, model, effort, opts.prompt ?? null, meta, cursorFast), () => resolveWinPaneShell("bash")));
|
|
6562
|
+
}
|
|
6563
|
+
else {
|
|
6564
|
+
const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta, cursorFast);
|
|
6565
|
+
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
6566
|
+
full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
6567
|
+
}
|
|
6568
|
+
if (meta)
|
|
6569
|
+
agentLaunchLines.set(sid, full);
|
|
6570
|
+
// force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
|
|
6571
|
+
send(sid, full, {
|
|
6572
|
+
enter: true,
|
|
6573
|
+
mark: false,
|
|
6574
|
+
force: true,
|
|
6575
|
+
rtk: false,
|
|
6576
|
+
raw: true,
|
|
6577
|
+
preserveAgentOperation: meta?.kind === "claude",
|
|
6578
|
+
});
|
|
6570
6579
|
}
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6580
|
+
catch (e) {
|
|
6581
|
+
const failure = e instanceof ExecutionError ? e : telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
|
|
6582
|
+
// 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
|
|
6583
|
+
try {
|
|
6584
|
+
closeSessionInternal(sid, false);
|
|
6585
|
+
}
|
|
6586
|
+
catch {
|
|
6587
|
+
/* 片付け失敗より元エラーの伝達を優先 */
|
|
6588
|
+
}
|
|
6589
|
+
throw failure;
|
|
6590
|
+
}
|
|
6591
|
+
const driveHint = agentDone
|
|
6592
|
+
? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
6593
|
+
`turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${autoDeliveryParent() ? `回答本文は${autoDeliveryParent()}親へ自動配送する` : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
|
|
6594
|
+
`Stopが来ない場合の解除はpty_close(${sid})を使う。`
|
|
6595
|
+
: `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
6596
|
+
`pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
|
|
6597
|
+
return [
|
|
6598
|
+
sid,
|
|
6599
|
+
`${label} を session ${sid} で起動した。${launchNote}${agentDone ? "agent_done 待機が有効。" : ""}` +
|
|
6600
|
+
`${agentDone ? " 通常のproject/user環境を共有し、aiterm所有の完了相関とsub-agent lineageだけを加算。" : ""}\n${hint}\n` +
|
|
6601
|
+
driveHint +
|
|
6602
|
+
`起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
|
|
6603
|
+
];
|
|
6604
|
+
};
|
|
6586
6605
|
}
|
|
6587
6606
|
export class AgentLaunchPromptError extends AitermError {
|
|
6588
6607
|
session_id;
|
|
@@ -6598,7 +6617,10 @@ export class AgentLaunchPromptError extends AitermError {
|
|
|
6598
6617
|
}
|
|
6599
6618
|
}
|
|
6600
6619
|
export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
6601
|
-
|
|
6620
|
+
if (opts.session_name)
|
|
6621
|
+
await withExecutionNativeAdmission(opts.session_name, () => assertExecutionContext(opts.session_name));
|
|
6622
|
+
else
|
|
6623
|
+
assertExecutionContext(opts.session_name);
|
|
6602
6624
|
const mission = opts.prompt ?? null;
|
|
6603
6625
|
const sourceSessionId = opts.throughline_source_session ?? null;
|
|
6604
6626
|
const supplementFile = opts.throughline_supplement_file ?? null;
|
|
@@ -6646,12 +6668,16 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
6646
6668
|
};
|
|
6647
6669
|
// GrokとCursorは、起動の --model の値を利用者の既定へ書く(Cursorは版による)。起動の前後で、直前の値へ戻す。
|
|
6648
6670
|
const launch = () => {
|
|
6671
|
+
const admitted = async () => {
|
|
6672
|
+
const create = prepareAgentLaunch(kind, openOptions);
|
|
6673
|
+
return opts.session_name ? withExecutionNativeAdmission(opts.session_name, create) : create();
|
|
6674
|
+
};
|
|
6649
6675
|
if (kind === "grok") {
|
|
6650
6676
|
const configPath = grokUserConfigPath(realGrokHome());
|
|
6651
6677
|
// Grokは起動完了の直後に書く(入力待ちと読める頃には書き終えている)。
|
|
6652
6678
|
return launchKeepingUserDefaults(configPath, {
|
|
6653
6679
|
restore: restoreGrokDefaults(configPath), written: grokDefaultsWritten, mustWrite: () => false, ready: grokTuiReady,
|
|
6654
|
-
},
|
|
6680
|
+
}, admitted);
|
|
6655
6681
|
}
|
|
6656
6682
|
if (kind === "cursor" && openOptions.model) {
|
|
6657
6683
|
const configPath = cursorUserConfigPath(realCursorHome());
|
|
@@ -6660,9 +6686,9 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
6660
6686
|
restore: restoreCursorDefaults(configPath), written: cursorDefaultsWritten,
|
|
6661
6687
|
mustWrite: (before) => { const config = parseJsonObject(before); return config !== null && "selectedModel" in config; },
|
|
6662
6688
|
ready: cursorTuiReady,
|
|
6663
|
-
},
|
|
6689
|
+
}, admitted);
|
|
6664
6690
|
}
|
|
6665
|
-
return
|
|
6691
|
+
return admitted();
|
|
6666
6692
|
};
|
|
6667
6693
|
// 入力待ちになった席を、その席の中でfastへ切り替える(Claude Code・Codex)。出来なかった訳は返りへ入れ、起動は続ける。
|
|
6668
6694
|
const switchAfterReady = async (name, meta) => {
|
package/dist/execution-close.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
// Synchronous expected-generation close. The serving adapter must already own
|
|
2
2
|
// the shared issued close slot; this helper does not replace admission/drain.
|
|
3
3
|
import { executionLaunchSchema } from "./execution-contract.js";
|
|
4
|
+
export { executionNativeBindingSchema } from "./execution-contract.js";
|
|
4
5
|
import { ExecutionError } from "./execution-wire.js";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
export const executionNativeBindingSchema = z.object({ schema: z.literal("aiterm.execution-native.v1"),
|
|
7
|
-
native_id: z.string().regex(/^\$\d+$/),
|
|
8
|
-
server: z.object({ pid: z.number().int().positive(), started_identity: z.string().min(1).max(256) }).strict(),
|
|
9
|
-
}).strict();
|
|
10
6
|
export function parseExecutionNativeSessions(text) {
|
|
11
7
|
const sessions = [];
|
|
12
8
|
for (const line of text.split(/\r?\n/).filter(Boolean)) {
|
|
@@ -7,6 +7,10 @@ const referenceId = z.string().min(1).max(256);
|
|
|
7
7
|
export const executionLaunchSchema = z.object({
|
|
8
8
|
session_id: sessionId, launch_id: launchId,
|
|
9
9
|
}).strict();
|
|
10
|
+
export const executionNativeBindingSchema = z.object({ schema: z.literal("aiterm.execution-native.v1"),
|
|
11
|
+
native_id: z.string().regex(/^\$\d+$/),
|
|
12
|
+
server: z.object({ pid: z.number().int().positive(), started_identity: z.string().min(1).max(256) }).strict(),
|
|
13
|
+
}).strict();
|
|
10
14
|
// Existing references, normalized by the runtime adapter to an immutable run
|
|
11
15
|
// boundary. A later native turn ID is an alias, not permission to rekey history.
|
|
12
16
|
export const executionBoundarySchema = z.object({
|
|
@@ -123,6 +123,13 @@ export function createExecutionDispatcher(bindings) {
|
|
|
123
123
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
124
124
|
bindings.assertParticipant?.();
|
|
125
125
|
return { sourceContext: extra => executionHostSourceFromMeta(extra._meta, bindings.initializedClientName()),
|
|
126
|
+
closeManagementRequest: (extra, session) => {
|
|
127
|
+
if (!registeredControls || !native || bindings.initializedClientName() !== "bellteam")
|
|
128
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
129
|
+
return executionRequestSchema.options[4].parse({ kind: "mcp", process_pid: native.pid,
|
|
130
|
+
process_started_identity: native.started_identity, request_id: extra.requestId,
|
|
131
|
+
tool: "pty_close", target_session_id: session });
|
|
132
|
+
},
|
|
126
133
|
activate: () => { registeredControls = true; } };
|
|
127
134
|
},
|
|
128
135
|
async dispatch(name, input, extra, body) {
|
|
@@ -7,3 +7,6 @@ export const createdLaunchObservationSchema = z.object({
|
|
|
7
7
|
initial_prompt: z.literal("none"), initial_prompt_delivery: z.null(),
|
|
8
8
|
harness_alive: z.literal(true), ready: z.literal(true),
|
|
9
9
|
}).strict();
|
|
10
|
+
export const exitedCreatedLaunchObservationSchema = createdLaunchObservationSchema.extend({
|
|
11
|
+
harness_alive: z.literal(false), ready: z.literal(false),
|
|
12
|
+
});
|
|
@@ -38,6 +38,7 @@ export function createNativeExecutionServer(input) {
|
|
|
38
38
|
selectAuthSessionName: core.selectExecutionAuthSessionName,
|
|
39
39
|
observeAuthSessionGeneration: core.observeExecutionAuthSessionGeneration,
|
|
40
40
|
observeCreatedLaunchSync: core.observeExecutionCreatedLaunch,
|
|
41
|
+
observeExitedCreatedLaunchSync: core.observeExecutionExitedCreatedLaunch,
|
|
41
42
|
closeExpectedSession: core.closeExpectedAgentSession, // explicit management only, never stop fallback
|
|
42
43
|
observeInterruptionSync: core.observeExpectedAgentRunInterruption,
|
|
43
44
|
selectInterruptionSync: core.selectExpectedAgentRunInterruption,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Private same-scope MCP parent producer. No public/index/profile mount. Queue
|
|
2
2
|
// aliases lacking current independent native proof remain unsupported.
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { observeAgentDone, selectExecutionAuthSessionName, observeExecutionAuthSessionGeneration, observeExecutionNativeCreationScope, closeExpectedAgentSession, observeExecutionCreatedLaunch } from "./core.js";
|
|
4
|
+
import { observeAgentDone, selectExecutionAuthSessionName, observeExecutionAuthSessionGeneration, observeExecutionNativeCreationScope, closeExpectedAgentSession, observeExecutionCreatedLaunch, observeExecutionExitedCreatedLaunch } from "./core.js";
|
|
5
5
|
import { codexParentFromRequest } from "./codex-parent-receiver.js";
|
|
6
6
|
import { claudeParentFromRequest } from "./claude-parent-receiver.js";
|
|
7
7
|
import { cursorParentFromRequest } from "./cursor-parent-receiver.js";
|
|
@@ -33,15 +33,17 @@ const localProducerRuntime = {
|
|
|
33
33
|
selectAuthSessionName: selectExecutionAuthSessionName,
|
|
34
34
|
observeAuthSessionGeneration: observeExecutionAuthSessionGeneration,
|
|
35
35
|
observeCreatedLaunchSync: observeExecutionCreatedLaunch,
|
|
36
|
+
observeExitedCreatedLaunchSync: observeExecutionExitedCreatedLaunch,
|
|
36
37
|
closeExpectedSession: closeExpectedAgentSession,
|
|
37
38
|
};
|
|
38
39
|
export function createExecutionParentProducer(input) {
|
|
39
40
|
const runtime = input.runtime ?? localProducerRuntime;
|
|
40
|
-
for (const name of ["selectAuthSessionName", "observeAuthSessionGeneration", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"])
|
|
41
|
+
for (const name of ["selectAuthSessionName", "observeAuthSessionGeneration", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync", "observeExitedCreatedLaunchSync"])
|
|
41
42
|
if (runtime[name] !== undefined && typeof runtime[name] !== "function")
|
|
42
43
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
43
44
|
const selectAuthName = runtime.selectAuthSessionName?.bind(runtime), observeAuth = runtime.observeAuthSessionGeneration?.bind(runtime);
|
|
44
45
|
const createdLaunch = runtime.observeCreatedLaunchSync?.bind(runtime);
|
|
46
|
+
const exitedLaunch = runtime.observeExitedCreatedLaunchSync?.bind(runtime);
|
|
45
47
|
const interruption = runtime.observeInterruptionSync?.bind(runtime);
|
|
46
48
|
const selectInterruption = runtime.selectInterruptionSync?.bind(runtime), interruptKey = runtime.interruptKeySync?.bind(runtime);
|
|
47
49
|
const observeActors = runtime.observeActors?.bind(runtime);
|
|
@@ -73,6 +75,14 @@ export function createExecutionParentProducer(input) {
|
|
|
73
75
|
} } : {}),
|
|
74
76
|
...(interruptKey ? { interruptKeySync: (point) => { assertScope(); return interruptKey(point); } } : {}),
|
|
75
77
|
observeSession: observe, observeSessionSync: observe,
|
|
78
|
+
...(runtime.closeExpectedSession ? { closeCreatedLaunchSync: (target) => {
|
|
79
|
+
assertScope();
|
|
80
|
+
return runtime.closeExpectedSession(target);
|
|
81
|
+
} } : {}),
|
|
82
|
+
...(exitedLaunch ? { observeExitedCreatedLaunchSync: (target) => {
|
|
83
|
+
assertScope();
|
|
84
|
+
return exitedLaunch(target);
|
|
85
|
+
} } : {}),
|
|
76
86
|
...(createdLaunch ? { observeCreatedLaunchSync: (target) => {
|
|
77
87
|
assertScope();
|
|
78
88
|
return createdLaunch(target);
|
|
@@ -12,7 +12,7 @@ function snapshotRuntime(input) {
|
|
|
12
12
|
for (const name of functions)
|
|
13
13
|
if (typeof input[name] !== "function")
|
|
14
14
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
15
|
-
const optional = ["selectAuthSessionName", "observeAuthSessionGeneration", "observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"];
|
|
15
|
+
const optional = ["selectAuthSessionName", "observeAuthSessionGeneration", "observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync", "observeExitedCreatedLaunchSync"];
|
|
16
16
|
for (const name of optional)
|
|
17
17
|
if (input[name] !== undefined && typeof input[name] !== "function")
|
|
18
18
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
@@ -45,7 +45,7 @@ const waitSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
47
47
|
export function createExecutionRuntime(bindings) {
|
|
48
|
-
for (const name of ["observeCreatedLaunchSync", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
|
|
48
|
+
for (const name of ["observeCreatedLaunchSync", "observeExitedCreatedLaunchSync", "closeCreatedLaunchSync", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
|
|
49
49
|
if (bindings[name] !== undefined && typeof bindings[name] !== "function")
|
|
50
50
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
51
51
|
const interruption = bindings.observeInterruptionSync;
|
|
@@ -72,8 +72,10 @@ export function createExecutionRuntime(bindings) {
|
|
|
72
72
|
return { state: "unknown" };
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
-
const created = bindings.observeCreatedLaunchSync;
|
|
75
|
+
const created = bindings.observeCreatedLaunchSync, exited = bindings.observeExitedCreatedLaunchSync;
|
|
76
76
|
return {
|
|
77
|
+
...(bindings.closeCreatedLaunchSync ? { closeCreatedLaunchSync: (target) => bindings.closeCreatedLaunchSync(executionLaunchSchema.parse(target)) } : {}),
|
|
78
|
+
...(exited ? { observeExitedCreatedLaunchSync: (target) => exited(executionLaunchSchema.parse(target)) } : {}),
|
|
77
79
|
...(created ? { observeCreatedLaunchSync: (target) => created(executionLaunchSchema.parse(target)) } : {}),
|
|
78
80
|
...(interruption ? { interruptionSync: (point) => interruption(executionPointSchema.parse(point)) } : {}),
|
|
79
81
|
...(selectInterruption ? { selectInterruptionSync: (expected, candidates) => selectInterruption(executionLaunchSchema.parse(expected), z.array(executionPointSchema).parse(candidates ?? [])) } : {}),
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Metadata/admission service with injected runtime. It never owns a payload or
|
|
2
2
|
// retries a launch/send/close. The core/MCP adapter is a separate integration.
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { assessExecutionSource, planCascadeStop, unfinishedExecutionRelations, cascadeStopRequestSchema, cascadeStopResultSchema, cascadeInterruptRequestSchema, cascadeInterruptResultSchema, executionPointSchema, executionLaunchSchema } from "./execution-contract.js";
|
|
4
|
+
import { assessExecutionSource, planCascadeStop, unfinishedExecutionRelations, cascadeStopRequestSchema, cascadeStopResultSchema, cascadeInterruptRequestSchema, cascadeInterruptResultSchema, executionPointSchema, executionLaunchSchema, executionRequestSchema } from "./execution-contract.js";
|
|
5
5
|
import { executionActionSchema, executionRecordSchema, executionRefMetaSchema, executionRootsRequestSchema, executionRootsResultSchema, ExecutionError } from "./execution-wire.js";
|
|
6
|
-
import { withExecutionEffectScope, withExecutionInterruptEffectScope } from "./execution-effect.js";
|
|
6
|
+
import { withExecutionEffectScope, withExecutionInterruptEffectScope, withExecutionNativeAdmission, bindExecutionManagementCloseTarget } from "./execution-effect.js";
|
|
7
7
|
import { nativeInterruptionObservationSchema, interruptionIdentity, interruptionBlocksLaunch, interruptionBlocksRequest, interruptionSlots, assertInterruptionSource, sameInterruptionRowRun } from "./execution-interruption.js";
|
|
8
8
|
import { interruptionKeyReference } from "./execution-interruption.js";
|
|
9
9
|
import { isDeepStrictEqual } from "node:util";
|
|
10
10
|
import { executionActorSchema } from "./execution-inventory.js";
|
|
11
|
-
import { createdLaunchObservationSchema } from "./execution-launch-recovery.js";
|
|
11
|
+
import { createdLaunchObservationSchema, exitedCreatedLaunchObservationSchema } from "./execution-launch-recovery.js";
|
|
12
12
|
import { executionCheckpoint } from "./execution-checkpoint.js";
|
|
13
|
+
import { executionCloseLaunchSchema, executionClosedLaunchSchema } from "./execution-wire.js";
|
|
13
14
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
14
15
|
const key = (value) => JSON.stringify(value);
|
|
15
16
|
const stopped = (state, launch) => !!launch && (state.stopping.some(value => same(value, launch)) || interruptionBlocksLaunch(state, launch));
|
|
@@ -43,29 +44,50 @@ function find(state, request) {
|
|
|
43
44
|
return record;
|
|
44
45
|
}
|
|
45
46
|
const managementClose = (request) => request.kind === "mcp" && request.tool === "pty_close";
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
/** A returned unknown send, or an empty exited created launch with additional
|
|
48
|
+
* native proof, may be displaced by explicit close of that exact generation.
|
|
49
|
+
* Issued work and other generations remain blockers; failed records are kept.
|
|
49
50
|
*/
|
|
50
|
-
function
|
|
51
|
+
function recoverableCloseSlots(state, request, session, expected) {
|
|
51
52
|
if (!managementClose(request) || expected === null)
|
|
52
53
|
return [];
|
|
53
54
|
return state.slots.filter(slot => {
|
|
54
|
-
if (slot.session_id !== session || slot.expected_launch_id !== expected || slot.stage
|
|
55
|
+
if (slot.session_id !== session || slot.expected_launch_id !== expected || !["send", "launch"].includes(slot.stage)
|
|
55
56
|
|| slot.state !== "unknown" || !slot.request || same(slot.request, request))
|
|
56
57
|
return false;
|
|
58
|
+
const original = state.records.find(record => same(record.reservation.request, slot.request));
|
|
59
|
+
if (slot.stage === "launch")
|
|
60
|
+
return !!original && !!slot.created_native_id && !!slot.created_server
|
|
61
|
+
&& (slot.request.kind !== "mcp" || slot.request.tool === "agent_launch")
|
|
62
|
+
&& original.operations.launch === "unknown" && original.operations.send === "not_issued"
|
|
63
|
+
&& original.relation.phase === "submitted_unknown" && original.relation.completion === "unknown"
|
|
64
|
+
&& original.relation.lifecycle === "tracking" && !openParentEffect(state, slot.request)
|
|
65
|
+
&& original.relation.target?.boundary === null
|
|
66
|
+
&& same(original.relation.target.launch, { session_id: session, launch_id: expected });
|
|
57
67
|
if (slot.request.kind === "mcp" && !["pty_send", "claude_turn.issue"].includes(slot.request.tool))
|
|
58
68
|
return false;
|
|
59
|
-
const original = state.records.find(record => same(record.reservation.request, slot.request));
|
|
60
69
|
return original?.operations.send === "unknown" && !["issued", "unknown"].includes(original.operations.launch)
|
|
61
70
|
&& original.relation.phase === "submitted_unknown" && original.relation.completion === "unknown"
|
|
62
71
|
&& same(original.relation.target?.launch, { session_id: session, launch_id: expected });
|
|
63
72
|
});
|
|
64
73
|
}
|
|
65
74
|
function targetBlocked(state, request, session, expected) {
|
|
66
|
-
const recoverable =
|
|
75
|
+
const recoverable = recoverableCloseSlots(state, request, session, expected);
|
|
67
76
|
return state.slots.some(slot => slot.session_id === session && !recoverable.includes(slot));
|
|
68
77
|
}
|
|
78
|
+
function assertCloseLaunchExpectation(state, expected, management) {
|
|
79
|
+
const original = find(state, expected.request), target = expected.target;
|
|
80
|
+
if (original.reservation.target.session_id !== target.session_id
|
|
81
|
+
|| !same(original.relation.target?.launch, target))
|
|
82
|
+
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
83
|
+
if (original.operations.launch === "issued" || original.operations.send === "issued")
|
|
84
|
+
throw new ExecutionError("EXECUTION_BUSY");
|
|
85
|
+
const slots = state.slots.filter(slot => slot.session_id === target.session_id && !same(slot.request, management));
|
|
86
|
+
const candidates = recoverableCloseSlots(state, management, target.session_id, target.launch_id);
|
|
87
|
+
if (slots.length !== 1 || !same(slots[0].request, expected.request) || slots[0].stage !== "launch"
|
|
88
|
+
|| !candidates.includes(slots[0]))
|
|
89
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
90
|
+
}
|
|
69
91
|
function view(state, record) {
|
|
70
92
|
return executionRecordSchema.parse({ ...record,
|
|
71
93
|
stopping: record.stopping || stopped(state, record.relation.source?.launch) || stopped(state, record.relation.target?.launch)
|
|
@@ -444,16 +466,30 @@ export class ExecutionService {
|
|
|
444
466
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
445
467
|
return this.store.transactionSync(state => {
|
|
446
468
|
const record = find(state, request), target = record.reservation.target;
|
|
447
|
-
if (!
|
|
469
|
+
if (!recoverableCloseSlots(state, request, target.session_id, target.expected_launch_id).length)
|
|
448
470
|
return undefined;
|
|
449
471
|
if (record.operations.send !== "preparing" || targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
450
472
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
451
473
|
const native = this.runtime.nativeSessionSync?.(target.session_id);
|
|
452
474
|
if (native?.state !== "present" || !native.server)
|
|
453
475
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
476
|
+
this.proveExitedLaunchClose(state, request, target.session_id, target.expected_launch_id, native);
|
|
454
477
|
return native.native_id;
|
|
455
478
|
}, { readOnly: true });
|
|
456
479
|
}
|
|
480
|
+
proveExitedLaunchClose(state, request, session, expected, native) {
|
|
481
|
+
const launches = recoverableCloseSlots(state, request, session, expected).filter(slot => slot.stage === "launch");
|
|
482
|
+
if (!launches.length)
|
|
483
|
+
return;
|
|
484
|
+
if (!expected || !this.runtime.observeExitedCreatedLaunchSync)
|
|
485
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
486
|
+
const target = { session_id: session, launch_id: expected };
|
|
487
|
+
const proof = exitedCreatedLaunchObservationSchema.safeParse(this.runtime.observeExitedCreatedLaunchSync(target));
|
|
488
|
+
if (!proof.success || !same(proof.data.target, target) || proof.data.native.native_id !== native.native_id
|
|
489
|
+
|| !same(proof.data.native.server, native.server) || launches.some(slot => slot.created_native_id !== native.native_id
|
|
490
|
+
|| !same(slot.created_server, native.server)))
|
|
491
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
492
|
+
}
|
|
457
493
|
verifyCreationServer(metaInput, expected) {
|
|
458
494
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
459
495
|
if (!this.store.transactionSync)
|
|
@@ -547,8 +583,52 @@ export class ExecutionService {
|
|
|
547
583
|
return this.store.transaction(state => view(state, find(state, args.request)), { readOnly: true });
|
|
548
584
|
if (args.action === "reconcile_launch")
|
|
549
585
|
return this.reconcileLaunch(args);
|
|
586
|
+
if (args.action === "close_launch")
|
|
587
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED"); // serving SDK identity required
|
|
550
588
|
return this.finish(args);
|
|
551
589
|
}
|
|
590
|
+
/** One explicit host RPC. The expectation is checked within registration,
|
|
591
|
+
* every effect admission and final commit; it never becomes a new source or
|
|
592
|
+
* rekeys the original failed request. No successful receipt survives a failed commit.
|
|
593
|
+
*/
|
|
594
|
+
async closeLaunch(input, managementInput, context) {
|
|
595
|
+
const expected = executionCloseLaunchSchema.parse(input), management = executionRequestSchema.options[4].parse(managementInput);
|
|
596
|
+
if (management.tool !== "pty_close" || management.target_session_id !== expected.target.session_id
|
|
597
|
+
|| same(management, expected.request) || !this.runtime.closeCreatedLaunchSync)
|
|
598
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
599
|
+
const source = context?.source ?? null;
|
|
600
|
+
await this.register({ action: "register", source_session_id: source?.launch.session_id ?? null,
|
|
601
|
+
reservation: { schema: "aiterm.execution-reservation.v1", request: management,
|
|
602
|
+
source: { launch: source?.launch ?? null, boundary: source?.boundary ?? null },
|
|
603
|
+
target: { session_id: expected.target.session_id, expected_launch_id: expected.target.launch_id } } }, context, expected);
|
|
604
|
+
const ref = { schema: "aiterm.execution-ref.v1", request: management };
|
|
605
|
+
const result = await this.invokeDeferred(ref, "send", (issue, prepare) => withExecutionEffectScope(expected.target.session_id, issue, async () => {
|
|
606
|
+
const native = await withExecutionNativeAdmission(expected.target.session_id, () => {
|
|
607
|
+
const id = this.managementCloseNative(ref);
|
|
608
|
+
if (!id)
|
|
609
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
610
|
+
const binding = this.store.transactionSync(state => {
|
|
611
|
+
assertCloseLaunchExpectation(state, expected, management);
|
|
612
|
+
const slot = state.slots.find(slot => same(slot.request, expected.request));
|
|
613
|
+
return { schema: "aiterm.execution-native.v1", native_id: slot.created_native_id, server: slot.created_server };
|
|
614
|
+
}, { readOnly: true });
|
|
615
|
+
bindExecutionManagementCloseTarget(expected.target.session_id, id);
|
|
616
|
+
const closed = this.runtime.closeCreatedLaunchSync(expected.target);
|
|
617
|
+
if (closed && typeof closed.then === "function") {
|
|
618
|
+
void Promise.resolve(closed).catch(() => undefined);
|
|
619
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED", "possible");
|
|
620
|
+
}
|
|
621
|
+
return binding;
|
|
622
|
+
});
|
|
623
|
+
return { value: native, target: { launch: expected.target, boundary: null } };
|
|
624
|
+
}, undefined, prepare, body => this.admitNative(body)), expected.target.session_id, undefined, expected);
|
|
625
|
+
return { record: result.record, receipt: executionClosedLaunchSchema.parse({
|
|
626
|
+
schema: "aiterm.closed-launch.v1", version: 1, action: "close_launch", outcome: "closed",
|
|
627
|
+
request: expected.request, target: expected.target, management_ref: management,
|
|
628
|
+
evidence: { native: result.value, initial_prompt: "none", initial_prompt_delivery: null,
|
|
629
|
+
harness_alive: false, absence_confirmed: true, metadata_committed: true },
|
|
630
|
+
}) };
|
|
631
|
+
}
|
|
552
632
|
async admitNative(body) {
|
|
553
633
|
if (!this.store.admitNative)
|
|
554
634
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
@@ -620,8 +700,10 @@ export class ExecutionService {
|
|
|
620
700
|
roots: selected, relation_coverage: !unresolved.length && coverage === "complete" ? "complete" : "incomplete", unresolved });
|
|
621
701
|
}, { readOnly: true });
|
|
622
702
|
}
|
|
623
|
-
async register(args, context) {
|
|
703
|
+
async register(args, context, closeExpectation) {
|
|
624
704
|
return this.store.transaction(async (state) => {
|
|
705
|
+
if (closeExpectation)
|
|
706
|
+
assertCloseLaunchExpectation(state, closeExpectation, args.reservation.request);
|
|
625
707
|
const previous = state.records.find(record => same(record.reservation.request, args.reservation.request));
|
|
626
708
|
if (previous) {
|
|
627
709
|
if (!same(previous.reservation, args.reservation) || previous.source_session_id !== args.source_session_id)
|
|
@@ -678,16 +760,18 @@ export class ExecutionService {
|
|
|
678
760
|
* issue() immediately before its first actual effect, on the same stack.
|
|
679
761
|
* Synchronous metadata admission persists issuance before any native action.
|
|
680
762
|
*/
|
|
681
|
-
async invokeDeferred(metaInput, stage, body, targetSessionId, admission) {
|
|
682
|
-
return this.invokeInternal(metaInput, stage, body, targetSessionId, true, admission);
|
|
763
|
+
async invokeDeferred(metaInput, stage, body, targetSessionId, admission, closeExpectation) {
|
|
764
|
+
return this.invokeInternal(metaInput, stage, body, targetSessionId, true, admission, closeExpectation);
|
|
683
765
|
}
|
|
684
|
-
async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission) {
|
|
766
|
+
async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission, closeExpectation) {
|
|
685
767
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
686
768
|
let expectedCloseNative;
|
|
687
769
|
if (deferred && (!this.store.transactionSync || !this.runtime.observeSync))
|
|
688
770
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
689
771
|
await this.store.transaction(async (state) => {
|
|
690
772
|
admission?.();
|
|
773
|
+
if (closeExpectation)
|
|
774
|
+
assertCloseLaunchExpectation(state, closeExpectation, request);
|
|
691
775
|
const record = find(state, request);
|
|
692
776
|
const relation = record.relation;
|
|
693
777
|
if (interruptionBlocksRequest(state, request))
|
|
@@ -704,13 +788,14 @@ export class ExecutionService {
|
|
|
704
788
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
705
789
|
if (deferred) {
|
|
706
790
|
const target = record.reservation.target;
|
|
707
|
-
if (
|
|
791
|
+
if (recoverableCloseSlots(state, request, target.session_id, target.expected_launch_id).length) {
|
|
708
792
|
if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
709
793
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
710
794
|
this.expectTarget(await this.runtime.observe(target.session_id), target.expected_launch_id, target.session_id);
|
|
711
795
|
const native = this.runtime.nativeSessionSync?.(target.session_id);
|
|
712
796
|
if (native?.state !== "present" || !native.server)
|
|
713
797
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
798
|
+
this.proveExitedLaunchClose(state, request, target.session_id, target.expected_launch_id, native);
|
|
714
799
|
expectedCloseNative = { native_id: native.native_id, server: structuredClone(native.server) };
|
|
715
800
|
}
|
|
716
801
|
record.operations[stage] = "preparing";
|
|
@@ -762,6 +847,8 @@ export class ExecutionService {
|
|
|
762
847
|
// wait between them. Owning its slot is not permission to ignore a
|
|
763
848
|
// stop, uncertain slot or generation replacement after the first one.
|
|
764
849
|
return this.store.transactionSync(state => {
|
|
850
|
+
if (closeExpectation)
|
|
851
|
+
assertCloseLaunchExpectation(state, closeExpectation, request);
|
|
765
852
|
const record = find(state, request), relation = record.relation;
|
|
766
853
|
if (interruptionBlocksRequest(state, request))
|
|
767
854
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
@@ -785,6 +872,8 @@ export class ExecutionService {
|
|
|
785
872
|
}
|
|
786
873
|
const current = this.runtime.observeSync(slot.session_id);
|
|
787
874
|
checkCloseNative(slot.session_id);
|
|
875
|
+
if (expectedCloseNative)
|
|
876
|
+
this.proveExitedLaunchClose(state, request, slot.session_id, slot.expected_launch_id, expectedCloseNative);
|
|
788
877
|
if (current.state === "present" && stopped(state, current.launch))
|
|
789
878
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
790
879
|
if (slot.created_native_id) {
|
|
@@ -808,6 +897,8 @@ export class ExecutionService {
|
|
|
808
897
|
}, { readOnly: true });
|
|
809
898
|
}
|
|
810
899
|
const result = this.store.transactionSync(state => {
|
|
900
|
+
if (closeExpectation)
|
|
901
|
+
assertCloseLaunchExpectation(state, closeExpectation, request);
|
|
811
902
|
const record = find(state, request), relation = record.relation;
|
|
812
903
|
if (interruptionBlocksRequest(state, request))
|
|
813
904
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
@@ -828,6 +919,8 @@ export class ExecutionService {
|
|
|
828
919
|
const current = this.runtime.observeSync(target.session_id);
|
|
829
920
|
this.expectTarget(current, expected, target.session_id);
|
|
830
921
|
checkCloseNative(target.session_id);
|
|
922
|
+
if (expectedCloseNative)
|
|
923
|
+
this.proveExitedLaunchClose(state, request, target.session_id, expected, expectedCloseNative);
|
|
831
924
|
if (current.state === "present" && stopped(state, current.launch))
|
|
832
925
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
833
926
|
if ((stage === "launch" && current.state !== "absent") || (stage === "send" && current.state !== "present"))
|
|
@@ -906,6 +999,8 @@ export class ExecutionService {
|
|
|
906
999
|
// Parsed above after issuance; null is supported only for an unissued noop.
|
|
907
1000
|
const targetPoint = outcome.target;
|
|
908
1001
|
const record = await this.store.transaction(async (state) => {
|
|
1002
|
+
if (closeExpectation)
|
|
1003
|
+
assertCloseLaunchExpectation(state, closeExpectation, request);
|
|
909
1004
|
const record = find(state, request);
|
|
910
1005
|
const slot = state.slots.find(value => same(value.request, request));
|
|
911
1006
|
if (!slot || slot.stage !== stage || record.operations[stage] !== "issued")
|
|
@@ -935,7 +1030,7 @@ export class ExecutionService {
|
|
|
935
1030
|
// Actual management close plus confirmed absence releases only these
|
|
936
1031
|
// failed send admissions. Old callbacks still fail their missing-slot
|
|
937
1032
|
// guard, and their records remain unknown and cannot be invoked again.
|
|
938
|
-
const recovered =
|
|
1033
|
+
const recovered = recoverableCloseSlots(state, request, slot.session_id, expected);
|
|
939
1034
|
state.slots = state.slots.filter(value => !recovered.includes(value));
|
|
940
1035
|
this.interruptRelations(state, targetPoint.launch);
|
|
941
1036
|
}
|
|
@@ -967,8 +1062,24 @@ export class ExecutionService {
|
|
|
967
1062
|
async reconcileLaunch(args) {
|
|
968
1063
|
return this.store.transaction(state => {
|
|
969
1064
|
const record = find(state, args.request), relation = record.relation;
|
|
970
|
-
if (record.operations.launch === "accepted" && same(relation.target?.launch, args.target))
|
|
1065
|
+
if (record.operations.launch === "accepted" && same(relation.target?.launch, args.target)) {
|
|
1066
|
+
// A saved accepted receipt is historical. Before a caller can use an
|
|
1067
|
+
// empty launch to resume input, prove that this generation is still
|
|
1068
|
+
// present and no later operation owns the same session slot.
|
|
1069
|
+
if (record.operations.send === "not_issued") {
|
|
1070
|
+
if (record.stopping || stopped(state, args.target) || stopped(state, relation.source?.launch)
|
|
1071
|
+
|| interruptionBlocksRequest(state, args.request))
|
|
1072
|
+
throw new ExecutionError("EXECUTION_STOPPING");
|
|
1073
|
+
if (state.slots.some(slot => slot.session_id === args.target.session_id) || openParentEffect(state, args.request))
|
|
1074
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
1075
|
+
if (!this.runtime.observeCreatedLaunchSync)
|
|
1076
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
1077
|
+
const parsed = createdLaunchObservationSchema.safeParse(this.runtime.observeCreatedLaunchSync(args.target));
|
|
1078
|
+
if (!parsed.success || !same(parsed.data.target, args.target))
|
|
1079
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
1080
|
+
}
|
|
971
1081
|
return view(state, record);
|
|
1082
|
+
}
|
|
972
1083
|
if (record.operations.launch === "issued")
|
|
973
1084
|
throw new ExecutionError("EXECUTION_BUSY");
|
|
974
1085
|
if (record.stopping || stopped(state, args.target) || stopped(state, relation.source?.launch)
|
package/dist/execution-tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { executionLaunchSchema, executionReservationSchema, executionRequestSchema, executionPointSchema, cascadeInterruptRequestSchema, cascadeInterruptResultObjectSchema } from "./execution-contract.js";
|
|
3
|
-
import { executionActionSchema, executionFinishSchema, executionRecordSchema, executionBindingSchema, executionRootsRequestSchema, executionRootsResultObjectSchema, executionRefFromMeta, executionErrorResult, ExecutionError, EXECUTION_BINDING_META_KEY, EXECUTION_ERROR_META_KEY, EXECUTION_SOURCE_META_KEY } from "./execution-wire.js";
|
|
3
|
+
import { executionActionSchema, executionFinishSchema, executionRecordSchema, executionBindingSchema, executionRootsRequestSchema, executionRootsResultObjectSchema, executionRefFromMeta, executionErrorResult, ExecutionError, EXECUTION_BINDING_META_KEY, EXECUTION_ERROR_META_KEY, EXECUTION_SOURCE_META_KEY, EXECUTION_CLOSED_LAUNCH_META_KEY } from "./execution-wire.js";
|
|
4
4
|
import { withExecutionEffectScope, bindExecutionManagementCloseTarget } from "./execution-effect.js";
|
|
5
5
|
import { immediateStopInputSchema, immediateStopResultSchema } from "./execution-immediate-stop.js";
|
|
6
6
|
export function executionBindingMeta(record) {
|
|
@@ -32,6 +32,14 @@ export function createExecutionToolHandlers(service, options = {}) {
|
|
|
32
32
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
33
33
|
// Capture and detach before entering the shared-store transaction.
|
|
34
34
|
const context = options.sourceContext?.(extra);
|
|
35
|
+
if (args.action === "close_launch") {
|
|
36
|
+
if (!options.closeManagementRequest)
|
|
37
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
38
|
+
const management = options.closeManagementRequest(extra, args.target.session_id);
|
|
39
|
+
const closed = await service.closeLaunch(args, management, context === undefined ? undefined : structuredClone(context));
|
|
40
|
+
return { content: [{ type: "text", text: JSON.stringify(closed.record) }], structuredContent: closed.record,
|
|
41
|
+
_meta: { ...executionBindingMeta(closed.record), [EXECUTION_CLOSED_LAUNCH_META_KEY]: closed.receipt } };
|
|
42
|
+
}
|
|
35
43
|
const result = await service.execute(args, context === undefined ? undefined : structuredClone(context));
|
|
36
44
|
return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result,
|
|
37
45
|
_meta: executionBindingMeta(result) };
|
|
@@ -65,12 +73,12 @@ export function createExecutionToolHandlers(service, options = {}) {
|
|
|
65
73
|
export function registerExecutionTools(server, service, options = {}) {
|
|
66
74
|
const registration = controlRegistrars.get(server)?.(service);
|
|
67
75
|
const handlers = createExecutionToolHandlers(service, { ...options,
|
|
68
|
-
...(registration ? { sourceContext: registration.sourceContext } : {}) });
|
|
76
|
+
...(registration ? { sourceContext: registration.sourceContext, closeManagementRequest: registration.closeManagementRequest } : {}) });
|
|
69
77
|
server.registerTool("agent_execution", {
|
|
70
|
-
description: "
|
|
78
|
+
description: "既存の実配送/実行参照を登録・照会・終了証拠で更新する。結果不明の空起動は元requestと期待世代を指定して照合、または終了済みCLIだけcloseする。本文や再送queueは保存しない。",
|
|
71
79
|
// MCP accepts a raw object shape; the handler enforces the stricter action
|
|
72
80
|
// union, including required fields and forbidden fields for each action.
|
|
73
|
-
inputSchema: { action: z.enum(["register", "status", "finish", "reconcile_launch"]),
|
|
81
|
+
inputSchema: { action: z.enum(["register", "status", "finish", "reconcile_launch", "close_launch"]),
|
|
74
82
|
reservation: executionReservationSchema.optional(), source_session_id: z.string().nullable().optional(),
|
|
75
83
|
request: executionRequestSchema.optional(), target: executionLaunchSchema.optional(), evidence: executionFinishSchema.shape.evidence.optional(),
|
|
76
84
|
remote: z.never().optional() },
|
package/dist/execution-wire.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Versioned Bridge/MCP contract. No dependency on core, PTY or the tool server.
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { executionLaunchSchema, executionPointSchema, executionRequestSchema, executionReservationSchema, executionRelationSchema, executionPhaseSchema, cascadeStopRequestSchema, cascadeStopResultSchema } from "./execution-contract.js";
|
|
3
|
+
import { executionLaunchSchema, executionPointSchema, executionRequestSchema, executionReservationSchema, executionRelationSchema, executionPhaseSchema, cascadeStopRequestSchema, cascadeStopResultSchema, executionNativeBindingSchema } from "./execution-contract.js";
|
|
4
4
|
export const EXECUTION_REQUEST_META_KEY = "aiterm/execution";
|
|
5
5
|
export const EXECUTION_BINDING_META_KEY = "aiterm/execution_binding";
|
|
6
6
|
export const EXECUTION_ERROR_META_KEY = "aiterm/execution_error";
|
|
7
7
|
export const EXECUTION_SOURCE_META_KEY = "aiterm/execution_source";
|
|
8
|
+
export const EXECUTION_CLOSED_LAUNCH_META_KEY = "aiterm/closed_launch";
|
|
8
9
|
export const executionTransportSourceSchema = z.object({
|
|
9
10
|
schema: z.literal("aiterm.execution-source.v1"), source: executionPointSchema.nullable(),
|
|
10
11
|
}).strict();
|
|
@@ -46,7 +47,22 @@ export const executionFinishSchema = z.object({
|
|
|
46
47
|
export const executionReconcileLaunchSchema = z.object({
|
|
47
48
|
action: z.literal("reconcile_launch"), request: executionRequestSchema, target: executionLaunchSchema,
|
|
48
49
|
}).strict();
|
|
49
|
-
export const
|
|
50
|
+
export const executionCloseLaunchSchema = z.object({
|
|
51
|
+
action: z.literal("close_launch"), request: executionRequestSchema, target: executionLaunchSchema,
|
|
52
|
+
}).strict();
|
|
53
|
+
export const executionClosedLaunchSchema = z.object({
|
|
54
|
+
schema: z.literal("aiterm.closed-launch.v1"), version: z.literal(1), action: z.literal("close_launch"),
|
|
55
|
+
outcome: z.literal("closed"), request: executionRequestSchema, target: executionLaunchSchema,
|
|
56
|
+
management_ref: executionRequestSchema.options[4],
|
|
57
|
+
evidence: z.object({ native: executionNativeBindingSchema, initial_prompt: z.literal("none"),
|
|
58
|
+
initial_prompt_delivery: z.null(), harness_alive: z.literal(false), absence_confirmed: z.literal(true),
|
|
59
|
+
metadata_committed: z.literal(true) }).strict(),
|
|
60
|
+
}).strict().superRefine((value, ctx) => {
|
|
61
|
+
if (value.management_ref.tool !== "pty_close" || value.management_ref.target_session_id !== value.target.session_id
|
|
62
|
+
|| (value.request.kind === "aiterm" ? value.request.launch.session_id : value.request.target_session_id) !== value.target.session_id)
|
|
63
|
+
ctx.addIssue({ code: "custom", message: "close receipt must bind the same original target and management request" });
|
|
64
|
+
});
|
|
65
|
+
export const executionActionSchema = z.union([executionRegisterSchema, executionStatusSchema, executionFinishSchema, executionReconcileLaunchSchema, executionCloseLaunchSchema]);
|
|
50
66
|
export const executionRefMetaSchema = z.object({
|
|
51
67
|
schema: z.literal("aiterm.execution-ref.v1"), request: executionRequestSchema,
|
|
52
68
|
}).strict();
|
package/docs/DESIGN.md
CHANGED
|
@@ -453,11 +453,25 @@ unknown send枠を保持したまま既存core closeへ進み、native close成
|
|
|
453
453
|
旧callbackは未知/消失した元slotで拒否され、同IDの新launchへnative作用を発行できない。
|
|
454
454
|
issued、管理close自体のunknown、親作用のslot、世代/native/serverの不明・交代は対象外。
|
|
455
455
|
既存の送信・待機lockの拒否も維持し、他席やissuerの停止を復旧条件へ追加しない。
|
|
456
|
+
返却済みunknown launchも、元native/server・managed世代が一致し、raw initial_prompt:none・初期配送なし・
|
|
457
|
+
send:not_issuedで、harnessの終了を現在の同期primitiveが証明できる場合だけ、同じ明示管理closeを許す(ADR 0131)。
|
|
458
|
+
受付時とnative作用直前にこの証拠を検査し、closeと不在確認後に該当slotを外す。元unknown recordを保ち、
|
|
459
|
+
旧requestのlaunch/sendは再実行しない。生存harnessはreconcile_launchの対象として維持する。
|
|
460
|
+
本体からの明示復旧には既存agent_executionのclose_launch actionを使う(ADR 0132)。元requestと
|
|
461
|
+
target(session_id,launch_id)を原子的な期待条件として受付と作用直前に照合し、応答SDK自身の管理refで
|
|
462
|
+
既存close primitiveへ進む。呼出元がobserveしたsessionだけをcloseする手順で代用しない。
|
|
463
|
+
close・不在確認・metadata commit後だけ_meta["aiterm/closed_launch"]のversion1 receiptを返す。
|
|
464
|
+
structuredContentは別管理requestのrecord、元unknown recordは不変。結果不明や保存失敗にはclosed証明なし。
|
|
465
|
+
本体は元pending操作・stop/versionを別途照合し、後発不明を過去receiptで解除しない。
|
|
466
|
+
非同期launcherはCLIの検証をgraph mutex外で行い、実create→metadata公開→世代結合→CLI入力の同期区間を
|
|
467
|
+
一度のnative admission内で通す。起動準備のawaitをmutex内に持ち込まず、待機後には通常の世代・停止・slot検査を行う。
|
|
456
468
|
空起動の結果不明は明示 `agent_execution/reconcile_launch` で既存世代へ結合できる(ADR 0128)。
|
|
457
469
|
対象は返却済みunknown launch、send:not_issued、元native/server stamp付きの単独slotだけ。
|
|
458
470
|
同じmetadata transaction内のprivate同期primitiveで現在のnative/managed世代、生のinitial_prompt:none・初期配送なし、
|
|
459
471
|
harness生存とidleを照合する。既存recordのlaunchをaccepted、targetを当該世代へ結合し、該当slotだけを外す。
|
|
460
472
|
これは現在世代の明示採用で、失われたRPC receiptの復元ではない。本文・初期prompt・claim・nativeには作用しない。
|
|
473
|
+
同じaccepted targetへの再照合もsend:not_issuedなら現在のmanaged世代・生の未入力状態・入力待ちを再確認し、
|
|
474
|
+
同じsessionへの競合slotや親の未確定作用があれば拒否する。保存済みacceptedを後の世代の不明解除の証拠にしない。
|
|
461
475
|
issued、入力済み・不明、停止、別slot、世代/native/serverの不一致は拒否。後続sendは元source世代を含め通常受付を通る。
|
|
462
476
|
遅着した元callbackはまだ所有するissued stage/slotがある時だけunknownを書け、照合後や別stageを上書きしない。
|
|
463
477
|
native handlerのisErrorは元の本文を保持し、`aiterm/execution_error`のcode/effectを併記する。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.61.
|
|
3
|
+
"version": "0.61.9",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|