aiterm-mcp 0.61.2 → 0.61.3
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 +9 -1
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/dist/core.js +7 -2
- package/dist/execution-dispatch.js +16 -5
- package/dist/execution-effect.js +16 -1
- package/dist/execution-service.js +78 -3
- package/dist/execution-tools.js +7 -2
- package/docs/DESIGN.md +9 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.3] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- An unknown send in the opt-in POSIX native execution entry no longer permanently blocks an explicit `pty_close` of that one session. The close is pinned to its expected launch/native identity; only confirmed close and absence release the failed-send admission. The original request remains unknown and is never replayed. Issued work, unknown management operations, and uncertain identities stay blocked.
|
|
15
|
+
- Native handler errors retain their original explanation alongside the typed execution error. A failed recovery close preserves the old launch metadata and reports the native failure.
|
|
16
|
+
|
|
10
17
|
## [0.61.2] - 2026-10-22
|
|
11
18
|
|
|
12
19
|
### 修正
|
|
@@ -2715,7 +2722,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2715
2722
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2716
2723
|
provenance.
|
|
2717
2724
|
|
|
2718
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2725
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.3...HEAD
|
|
2726
|
+
[0.61.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.2...v0.61.3
|
|
2719
2727
|
[0.61.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.1...v0.61.2
|
|
2720
2728
|
[0.61.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.0...v0.61.1
|
|
2721
2729
|
[0.61.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.60.1...v0.61.0
|
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.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -731,7 +731,7 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
|
|
|
731
731
|
|
|
732
732
|
`pty_send` はコマンドやpromptの意味を判定せず、指定された本文を端末へ送る。既定ではESC・ブラケットペースト終端などをサニタイズし、`pty_read`も制御文字を無害化して返す(`raw: true`はそのまま扱う)。コマンドの許可・拒否はshell、接続先、起動したharnessが所有する。
|
|
733
733
|
|
|
734
|
-
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellまたはPowerShellが前面にいる時のsanitize済み複数行は、改行を含まない単一入力へ符号化する。POSIXは`eval`、PowerShellはUTF-8のBase64を復元したscriptblockのdot-sourceを使い、変数と作業場所を現在のshellに保つ。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatchは本文全体を`ESC[200~/201~`で1回だけ包み、包んだ全体を同じchunkで流す。agent TUIには1回の貼り付けに見えるので画像pathが貼り付けの間で切れず、chunk投入中のキー解釈による語中文字化け・submit取り落としも抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。`pty_list`で対象sessionを確認し、`pty_close`で閉じてから同じsession IDを作り直す。公開の一括停止toolは存在しない。
|
|
734
|
+
1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。全OSで長いPTY入力の欠落を避けるためplatformのmultiplexerへUTF-8境界を壊さない256-byte単位でpasteし、chunk間に10msのdrain間隔を置く。POSIX shellまたはPowerShellが前面にいる時のsanitize済み複数行は、改行を含まない単一入力へ符号化する。POSIXは`eval`、PowerShellはUTF-8のBase64を復元したscriptblockのdot-sourceを使い、変数と作業場所を現在のshellに保つ。shellがscript全体を所有してから先頭行を実行するため、途中で起動したpager/REPLが後続行を対話キーとして奪わない。単一行、`raw:true`、非shell前面は従来どおり直接PTYへpasteする。agent dispatchは本文全体を`ESC[200~/201~`で1回だけ包み、包んだ全体を同じchunkで流す。agent TUIには1回の貼り付けに見えるので画像pathが貼り付けの間で切れず、chunk投入中のキー解釈による語中文字化け・submit取り落としも抑える。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。`pty_list`で対象sessionを確認し、`pty_close`で閉じてから同じsession IDを作り直す。公開の一括停止toolは存在しない。 POSIXの明示native execution構成では、unknown sendになった対象1席も`pty_close`で閉じて再作成できる。元requestはunknownのまま保持し、再送しない。発行中の操作、管理close自体のunknown、native identityが不明な席は引き続き拒否する。
|
|
735
735
|
|
|
736
736
|
## 人が覗く
|
|
737
737
|
|
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.3** · 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
|
|
|
@@ -789,7 +789,7 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
|
|
|
789
789
|
|
|
790
790
|
`pty_send` does not interpret command or prompt meaning; it delivers the requested text to the terminal. By default it sanitizes ESC and bracketed-paste terminators, while `pty_read` neutralizes control characters in returned output (`raw: true` keeps them unchanged). The shell, remote endpoint, or launched harness owns command authorization.
|
|
791
791
|
|
|
792
|
-
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through its multiplexer in UTF-8-safe 256-byte chunks with a 10 ms drain interval; macOS, Linux, and WSL2 have all demonstrated silent middle/trailing loss when a long input is pushed without that boundary. Sanitized multiline text sent while a POSIX shell or PowerShell is in the foreground is encoded as one newline-free input (`eval` for POSIX shells; a dot-sourced, UTF-8 Base64-decoded scriptblock for PowerShell): the shell receives the complete script before it runs the first line, so a pager or REPL started mid-script cannot consume later lines as interactive keystrokes. Single-line input, `raw:true`, and non-shell frontends remain direct PTY pastes. Agent dispatches additionally wrap the whole text once in `ESC[200~/201~` and stream the wrapped text in the same chunks, so the agent TUI sees one paste (an image path is never split across pastes), hardening prompt injection against mid-word key-interpretation corruption and dropped submits. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; use `pty_list` to confirm the affected session, close it with `pty_close`, then recreate the same session ID. There is no public kill-all tool.
|
|
792
|
+
Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. Every OS pastes through its multiplexer in UTF-8-safe 256-byte chunks with a 10 ms drain interval; macOS, Linux, and WSL2 have all demonstrated silent middle/trailing loss when a long input is pushed without that boundary. Sanitized multiline text sent while a POSIX shell or PowerShell is in the foreground is encoded as one newline-free input (`eval` for POSIX shells; a dot-sourced, UTF-8 Base64-decoded scriptblock for PowerShell): the shell receives the complete script before it runs the first line, so a pager or REPL started mid-script cannot consume later lines as interactive keystrokes. Single-line input, `raw:true`, and non-shell frontends remain direct PTY pastes. Agent dispatches additionally wrap the whole text once in `ESC[200~/201~` and stream the wrapped text in the same chunks, so the agent TUI sees one paste (an image path is never split across pastes), hardening prompt injection against mid-word key-interpretation corruption and dropped submits. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; use `pty_list` to confirm the affected session, close it with `pty_close`, then recreate the same session ID. There is no public kill-all tool. In the opt-in POSIX native execution entry, an unknown send can also be recovered by explicitly closing that one session with `pty_close`, then recreating it. The original request remains unknown and must not be replayed. Issued work, unknown management closes, and uncertain native identity remain blocked.
|
|
793
793
|
|
|
794
794
|
## A human can watch
|
|
795
795
|
|
package/dist/core.js
CHANGED
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import * as rtk from "./rtk.js";
|
|
17
17
|
import { closeExpectedExecution, parseExecutionNativeSessions, executionNativeBindingSchema } from "./execution-close.js";
|
|
18
18
|
import { executionPointSchema } from "./execution-contract.js";
|
|
19
|
-
import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary } from "./execution-effect.js";
|
|
19
|
+
import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary, executionManagementCloseTarget } from "./execution-effect.js";
|
|
20
20
|
import { ExecutionError } from "./execution-wire.js";
|
|
21
21
|
import { readExecutionParentProcess } from "./execution-parent-process.js";
|
|
22
22
|
import { isCursorMcpClient } from "aiterm-steer-delivery";
|
|
@@ -1869,7 +1869,12 @@ function closeSessionInternal(name, observeDependency = true, o = {}) {
|
|
|
1869
1869
|
assertSessionClosable(name);
|
|
1870
1870
|
if (o.letHarnessExit)
|
|
1871
1871
|
letClaudeExitBeforeClose(name);
|
|
1872
|
-
(observeDependency ? tmux : tmuxCleanup)("kill-session", "-t", name);
|
|
1872
|
+
const killed = (observeDependency ? tmux : tmuxCleanup)("kill-session", "-t", name);
|
|
1873
|
+
if (executionManagementCloseTarget(name) && killed.code !== 0) {
|
|
1874
|
+
// Recovery must not erase the old launch metadata or report closed when
|
|
1875
|
+
// its one pinned native close failed. Keep the original send uncertainty.
|
|
1876
|
+
throw new AitermError(`tmux sessionを閉じられませんでした: ${killed.stderr.trim() || `code=${killed.code}`}`, 2);
|
|
1877
|
+
}
|
|
1873
1878
|
for (const p of [logpath(name), offsetpath(name), lastcmdpath(name), markpath(name), sendLockPath(name)]) {
|
|
1874
1879
|
try {
|
|
1875
1880
|
fs.unlinkSync(p);
|
|
@@ -140,12 +140,15 @@ export function createExecutionDispatcher(bindings) {
|
|
|
140
140
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
141
141
|
const context = { schema: "aiterm.execution-source.v1", source };
|
|
142
142
|
const supplied = executionRefFromMeta(fixedExtra._meta);
|
|
143
|
+
// A host's explicit session close is a management RPC with its own
|
|
144
|
+
// existing process/request identity, not a delivery or resend.
|
|
145
|
+
const hostClose = client === "bellteam" && route.tool === "pty_close" && !supplied;
|
|
143
146
|
if (args.remote !== undefined && args.remote !== null)
|
|
144
147
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
145
148
|
let reference = supplied;
|
|
146
149
|
let directRequest = null;
|
|
147
150
|
let frozenParent = null;
|
|
148
|
-
if (!reference && client !== "bellteam" && native) {
|
|
151
|
+
if (!reference && (client !== "bellteam" || hostClose) && native) {
|
|
149
152
|
directRequest = executionRequestSchema.options[4].parse({ kind: "mcp", process_pid: native.pid,
|
|
150
153
|
process_started_identity: native.started_identity, request_id: fixedExtra.requestId,
|
|
151
154
|
tool: route.tool, target_session_id: route.target });
|
|
@@ -186,7 +189,7 @@ export function createExecutionDispatcher(bindings) {
|
|
|
186
189
|
}
|
|
187
190
|
else {
|
|
188
191
|
// BellTeam must retain its existing delivery/room/call reference.
|
|
189
|
-
if (client === "bellteam" || !directRequest)
|
|
192
|
+
if ((client === "bellteam" && !hostClose) || !directRequest)
|
|
190
193
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
191
194
|
const current = await observe(route.target);
|
|
192
195
|
if (route.stage === "send" && !current.point)
|
|
@@ -204,15 +207,21 @@ export function createExecutionDispatcher(bindings) {
|
|
|
204
207
|
const expected = status.relation.target?.launch ?? (status.reservation.target.expected_launch_id
|
|
205
208
|
? { session_id: route.target, launch_id: status.reservation.target.expected_launch_id } : null);
|
|
206
209
|
const requestMeta = { ...fixedExtra._meta, [EXECUTION_REQUEST_META_KEY]: reference };
|
|
207
|
-
|
|
210
|
+
let handlerFailure;
|
|
211
|
+
const bound = await runDeferredBoundExecution(bindings.service, requestMeta, route.stage, route.target, async () => {
|
|
208
212
|
const result = await withExecutionRequest(reference.request, () => body(args, { ...fixedExtra, _meta: requestMeta }), {
|
|
209
213
|
source: status.source.status === "verified" ? status.source.point : null,
|
|
210
214
|
client_name: client ?? null, native_identity: native,
|
|
211
215
|
metadata: fixedExtra._meta === undefined ? null : fixedExtra._meta,
|
|
212
216
|
...(captureNativeParent ? { native_parent: frozenParent } : {}),
|
|
213
217
|
});
|
|
214
|
-
if (result.isError)
|
|
218
|
+
if (result.isError) {
|
|
219
|
+
// Keep the original native/handler refusal for the caller. It is
|
|
220
|
+
// still an error, never an accepted receipt or replay permission;
|
|
221
|
+
// the service determines whether an effect crossed its gate.
|
|
222
|
+
handlerFailure = result;
|
|
215
223
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
224
|
+
}
|
|
216
225
|
// Missing/invalid binding evidence is returned as null. The wrapper
|
|
217
226
|
// retains the genuine successful receipt if binding then fails; it
|
|
218
227
|
// must not turn a valid accepted action into permission to resend.
|
|
@@ -263,6 +272,8 @@ export function createExecutionDispatcher(bindings) {
|
|
|
263
272
|
}
|
|
264
273
|
return { result, target };
|
|
265
274
|
}, cancellation?.assert);
|
|
275
|
+
return handlerFailure && bound.isError ? { ...handlerFailure,
|
|
276
|
+
_meta: { ...handlerFailure._meta, ...bound._meta } } : bound;
|
|
266
277
|
}
|
|
267
278
|
catch (error) {
|
|
268
279
|
return executionErrorResult(error);
|
|
@@ -276,7 +287,7 @@ export function createExecutionDispatcher(bindings) {
|
|
|
276
287
|
const failure = executionErrorResult(new ExecutionError("EXECUTION_UNCONFIRMED", "possible"));
|
|
277
288
|
// Preserve a genuine accepted result even when cancellation recording
|
|
278
289
|
// failed. Status-only uncertainty must never become permission to resend.
|
|
279
|
-
return
|
|
290
|
+
return { ...result, _meta: { ...result._meta,
|
|
280
291
|
[EXECUTION_ERROR_META_KEY]: failure._meta[EXECUTION_ERROR_META_KEY] } };
|
|
281
292
|
}
|
|
282
293
|
return result;
|
package/dist/execution-effect.js
CHANGED
|
@@ -286,6 +286,21 @@ export function bindExecutionNativeTarget(session, nativeId) {
|
|
|
286
286
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
287
287
|
scope.nativeTargets.add(nativeId);
|
|
288
288
|
}
|
|
289
|
+
/** Bound only by the management-close wrapper, never tool arguments. The
|
|
290
|
+
* ordinary core close/cleanup keeps using its session name; native commands
|
|
291
|
+
* are pinned to the independently captured native ID through the same gate.
|
|
292
|
+
*/
|
|
293
|
+
export function bindExecutionManagementCloseTarget(session, nativeId) {
|
|
294
|
+
const scope = scopes.getStore();
|
|
295
|
+
if (!scope || scope.session !== session || scope.didIssue || scope.creation || scope.interrupt || !/^\$\d+$/.test(nativeId))
|
|
296
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
297
|
+
scope.managementCloseTarget = nativeId;
|
|
298
|
+
scope.nativeTargets.add(nativeId);
|
|
299
|
+
}
|
|
300
|
+
export function executionManagementCloseTarget(session) {
|
|
301
|
+
const scope = scopes.getStore();
|
|
302
|
+
return scope?.session === session ? scope.managementCloseTarget : undefined;
|
|
303
|
+
}
|
|
289
304
|
export function beforeExecutionEffect(session, verify) {
|
|
290
305
|
const scope = scopes.getStore();
|
|
291
306
|
if (!scope) {
|
|
@@ -376,7 +391,7 @@ export function guardTmuxExecution(input) {
|
|
|
376
391
|
if (!parsed || parsed[1] !== scope.session)
|
|
377
392
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
378
393
|
if (command !== "new-session")
|
|
379
|
-
args[at + 1] = `${scope.createdNative ?? "=" + scope.session}${parsed[2] ?? ":"}`;
|
|
394
|
+
args[at + 1] = `${scope.managementCloseTarget ?? scope.createdNative ?? "=" + scope.session}${parsed[2] ?? ":"}`;
|
|
380
395
|
}
|
|
381
396
|
effects = true;
|
|
382
397
|
}
|
|
@@ -41,6 +41,30 @@ function find(state, request) {
|
|
|
41
41
|
throw new ExecutionError("EXECUTION_NOT_FOUND");
|
|
42
42
|
return record;
|
|
43
43
|
}
|
|
44
|
+
const managementClose = (request) => request.kind === "mcp" && request.tool === "pty_close";
|
|
45
|
+
/** Only a returned, unknown send may be displaced by an explicit close of
|
|
46
|
+
* that exact launch. Issued work, management/parent slots and other generations
|
|
47
|
+
* remain blockers. The record is never turned into accepted or not_sent.
|
|
48
|
+
*/
|
|
49
|
+
function recoverableSends(state, request, session, expected) {
|
|
50
|
+
if (!managementClose(request) || expected === null)
|
|
51
|
+
return [];
|
|
52
|
+
return state.slots.filter(slot => {
|
|
53
|
+
if (slot.session_id !== session || slot.expected_launch_id !== expected || slot.stage !== "send"
|
|
54
|
+
|| slot.state !== "unknown" || !slot.request || same(slot.request, request))
|
|
55
|
+
return false;
|
|
56
|
+
if (slot.request.kind === "mcp" && !["pty_send", "claude_turn.issue"].includes(slot.request.tool))
|
|
57
|
+
return false;
|
|
58
|
+
const original = state.records.find(record => same(record.reservation.request, slot.request));
|
|
59
|
+
return original?.operations.send === "unknown" && !["issued", "unknown"].includes(original.operations.launch)
|
|
60
|
+
&& original.relation.phase === "submitted_unknown" && original.relation.completion === "unknown"
|
|
61
|
+
&& same(original.relation.target?.launch, { session_id: session, launch_id: expected });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function targetBlocked(state, request, session, expected) {
|
|
65
|
+
const recoverable = recoverableSends(state, request, session, expected);
|
|
66
|
+
return state.slots.some(slot => slot.session_id === session && !recoverable.includes(slot));
|
|
67
|
+
}
|
|
44
68
|
function view(state, record) {
|
|
45
69
|
return executionRecordSchema.parse({ ...record,
|
|
46
70
|
stopping: record.stopping || stopped(state, record.relation.source?.launch) || stopped(state, record.relation.target?.launch)
|
|
@@ -389,6 +413,28 @@ export class ExecutionService {
|
|
|
389
413
|
return native.server;
|
|
390
414
|
}, { readOnly: true });
|
|
391
415
|
}
|
|
416
|
+
/** Private wrapper preparation for an explicit management close. No native
|
|
417
|
+
* effect or slot removal occurs here. The invoke gate independently rechecks
|
|
418
|
+
* this same identity immediately before every native command.
|
|
419
|
+
*/
|
|
420
|
+
managementCloseNative(metaInput) {
|
|
421
|
+
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
422
|
+
if (!managementClose(request))
|
|
423
|
+
return undefined;
|
|
424
|
+
if (!this.store.transactionSync)
|
|
425
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
426
|
+
return this.store.transactionSync(state => {
|
|
427
|
+
const record = find(state, request), target = record.reservation.target;
|
|
428
|
+
if (!recoverableSends(state, request, target.session_id, target.expected_launch_id).length)
|
|
429
|
+
return undefined;
|
|
430
|
+
if (record.operations.send !== "preparing" || targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
431
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
432
|
+
const native = this.runtime.nativeSessionSync?.(target.session_id);
|
|
433
|
+
if (native?.state !== "present" || !native.server)
|
|
434
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
435
|
+
return native.native_id;
|
|
436
|
+
}, { readOnly: true });
|
|
437
|
+
}
|
|
392
438
|
verifyCreationServer(metaInput, expected) {
|
|
393
439
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
394
440
|
if (!this.store.transactionSync)
|
|
@@ -564,7 +610,7 @@ export class ExecutionService {
|
|
|
564
610
|
}
|
|
565
611
|
sourceAdmission(state, source.point, args.source_session_id);
|
|
566
612
|
const target = args.reservation.target;
|
|
567
|
-
if (state.
|
|
613
|
+
if (targetBlocked(state, args.reservation.request, target.session_id, target.expected_launch_id))
|
|
568
614
|
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
569
615
|
const current = await this.runtime.observe(target.session_id);
|
|
570
616
|
this.expectTarget(current, target.expected_launch_id, target.session_id);
|
|
@@ -606,6 +652,7 @@ export class ExecutionService {
|
|
|
606
652
|
}
|
|
607
653
|
async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission) {
|
|
608
654
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
655
|
+
let expectedCloseNative;
|
|
609
656
|
if (deferred && (!this.store.transactionSync || !this.runtime.observeSync))
|
|
610
657
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
611
658
|
await this.store.transaction(async (state) => {
|
|
@@ -625,6 +672,16 @@ export class ExecutionService {
|
|
|
625
672
|
if (record.reservation.source.launch && !relation.source)
|
|
626
673
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
627
674
|
if (deferred) {
|
|
675
|
+
const target = record.reservation.target;
|
|
676
|
+
if (recoverableSends(state, request, target.session_id, target.expected_launch_id).length) {
|
|
677
|
+
if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
678
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
679
|
+
this.expectTarget(await this.runtime.observe(target.session_id), target.expected_launch_id, target.session_id);
|
|
680
|
+
const native = this.runtime.nativeSessionSync?.(target.session_id);
|
|
681
|
+
if (native?.state !== "present" || !native.server)
|
|
682
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
683
|
+
expectedCloseNative = { native_id: native.native_id, server: structuredClone(native.server) };
|
|
684
|
+
}
|
|
628
685
|
record.operations[stage] = "preparing";
|
|
629
686
|
return; // no native effect or target slot yet; stop can cancel this
|
|
630
687
|
}
|
|
@@ -654,6 +711,14 @@ export class ExecutionService {
|
|
|
654
711
|
state.slots.push({ session_id: target.session_id, expected_launch_id: expected, stage, state: "issued", request });
|
|
655
712
|
});
|
|
656
713
|
let issued = !deferred, finished = false;
|
|
714
|
+
const checkCloseNative = (session) => {
|
|
715
|
+
if (!expectedCloseNative)
|
|
716
|
+
return;
|
|
717
|
+
const native = this.runtime.nativeSessionSync?.(session);
|
|
718
|
+
if (native?.state !== "present" || native.native_id !== expectedCloseNative.native_id
|
|
719
|
+
|| !same(native.server, expectedCloseNative.server))
|
|
720
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
721
|
+
};
|
|
657
722
|
const checkOrIssue = (commit, preparation) => {
|
|
658
723
|
admission?.();
|
|
659
724
|
const prepared = () => { admission?.(); return preparation(); };
|
|
@@ -688,6 +753,7 @@ export class ExecutionService {
|
|
|
688
753
|
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
689
754
|
}
|
|
690
755
|
const current = this.runtime.observeSync(slot.session_id);
|
|
756
|
+
checkCloseNative(slot.session_id);
|
|
691
757
|
if (current.state === "present" && stopped(state, current.launch))
|
|
692
758
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
693
759
|
if (slot.created_native_id) {
|
|
@@ -725,11 +791,12 @@ export class ExecutionService {
|
|
|
725
791
|
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
726
792
|
}
|
|
727
793
|
const target = record.reservation.target;
|
|
728
|
-
if (state
|
|
794
|
+
if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
729
795
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
730
796
|
const expected = relation.target?.launch.launch_id ?? target.expected_launch_id;
|
|
731
797
|
const current = this.runtime.observeSync(target.session_id);
|
|
732
798
|
this.expectTarget(current, expected, target.session_id);
|
|
799
|
+
checkCloseNative(target.session_id);
|
|
733
800
|
if (current.state === "present" && stopped(state, current.launch))
|
|
734
801
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
735
802
|
if ((stage === "launch" && current.state !== "absent") || (stage === "send" && current.state !== "present"))
|
|
@@ -830,8 +897,14 @@ export class ExecutionService {
|
|
|
830
897
|
if (state.stopping.some(value => same(value, record.relation.source?.launch)) && !state.stopping.some(value => same(value, targetPoint.launch)))
|
|
831
898
|
state.stopping.push(targetPoint.launch);
|
|
832
899
|
state.slots = state.slots.filter(value => !same(value.request, request));
|
|
833
|
-
if (closing)
|
|
900
|
+
if (closing) {
|
|
901
|
+
// Actual management close plus confirmed absence releases only these
|
|
902
|
+
// failed send admissions. Old callbacks still fail their missing-slot
|
|
903
|
+
// guard, and their records remain unknown and cannot be invoked again.
|
|
904
|
+
const recovered = recoverableSends(state, request, slot.session_id, expected);
|
|
905
|
+
state.slots = state.slots.filter(value => !recovered.includes(value));
|
|
834
906
|
this.interruptRelations(state, targetPoint.launch);
|
|
907
|
+
}
|
|
835
908
|
return view(state, record);
|
|
836
909
|
}).catch(async () => {
|
|
837
910
|
// The side effect already returned, but its result could not be safely
|
|
@@ -1326,6 +1399,8 @@ export class ExecutionService {
|
|
|
1326
1399
|
interruptRelations(state, launch) {
|
|
1327
1400
|
for (const record of state.records)
|
|
1328
1401
|
if (same(record.relation.target?.launch, launch)) {
|
|
1402
|
+
if (record.operations.launch === "unknown" || record.operations.send === "unknown")
|
|
1403
|
+
continue;
|
|
1329
1404
|
record.relation.completion = "interrupted";
|
|
1330
1405
|
record.relation.target_fence = "closed";
|
|
1331
1406
|
}
|
package/dist/execution-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { executionReservationSchema, executionRequestSchema, executionPointSchema, cascadeInterruptRequestSchema, cascadeInterruptResultObjectSchema } from "./execution-contract.js";
|
|
3
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";
|
|
4
|
-
import { withExecutionEffectScope } from "./execution-effect.js";
|
|
4
|
+
import { withExecutionEffectScope, bindExecutionManagementCloseTarget } from "./execution-effect.js";
|
|
5
5
|
import { immediateStopInputSchema, immediateStopResultSchema } from "./execution-immediate-stop.js";
|
|
6
6
|
export function executionBindingMeta(record) {
|
|
7
7
|
return { [EXECUTION_BINDING_META_KEY]: executionBindingSchema.parse({
|
|
@@ -119,7 +119,12 @@ async function runExecutionReceipt(service, meta, stage, body, targetSessionId,
|
|
|
119
119
|
const result = deferred
|
|
120
120
|
? await service.invokeDeferred(ref, stage, async (issue, prepare) => {
|
|
121
121
|
let expectedServer;
|
|
122
|
-
const action = await withExecutionEffectScope(targetSessionId, issue,
|
|
122
|
+
const action = await withExecutionEffectScope(targetSessionId, issue, () => {
|
|
123
|
+
const native = service.managementCloseNative(ref);
|
|
124
|
+
if (native)
|
|
125
|
+
bindExecutionManagementCloseTarget(targetSessionId, native);
|
|
126
|
+
return run();
|
|
127
|
+
}, stage === "launch" ? {
|
|
123
128
|
prepare: () => { expectedServer = service.prepareCreation(ref); return expectedServer; },
|
|
124
129
|
verifyServer: () => { if (!expectedServer)
|
|
125
130
|
throw new ExecutionError("EXECUTION_UNSUPPORTED"); service.verifyCreationServer(ref, expectedServer); },
|
package/docs/DESIGN.md
CHANGED
|
@@ -424,6 +424,15 @@ ADR 0074(repositoryの`docs/adr/0074-agent-model-catalog.md`)。
|
|
|
424
424
|
境界失敗は明示errorにする。retry、別model、別harness、別backendへ自動fallbackしない。
|
|
425
425
|
stale send lockは並行processとのABAを避けるため自動削除せず、公開APIでは対象sessionを`pty_close`して
|
|
426
426
|
同じIDで再作成する。全session一括停止は公開しない。
|
|
427
|
+
POSIXの明示native execution構成では、返却済みsendのunknown枠が対象席の管理closeまで
|
|
428
|
+
永久に拒否しないよう、通常`pty_close`だけを同じ期待launch/native IDで受付できる。
|
|
429
|
+
unknown send枠を保持したまま既存core closeへ進み、native close成功と席の不在を確認した後だけ
|
|
430
|
+
該当send枠を解放する。元recordのphase/completion/sendはunknownのまま残し、再送や成功化をしない。
|
|
431
|
+
旧callbackは未知/消失した元slotで拒否され、同IDの新launchへnative作用を発行できない。
|
|
432
|
+
issued、管理close自体のunknown、親作用のslot、世代/native/serverの不明・交代は対象外。
|
|
433
|
+
既存の送信・待機lockの拒否も維持し、他席やissuerの停止を復旧条件へ追加しない。
|
|
434
|
+
native handlerのisErrorは元の本文を保持し、`aiterm/execution_error`のcode/effectを併記する。
|
|
435
|
+
native作用後の不明はpossibleのままで、原因説明を再送可否や成功receiptへ変換しない。
|
|
427
436
|
agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が長いので、持ち主が終了した残骸を次の送信が片付ける。消す役を`<name>.agent-send.lock.reap`の排他作成で1つに絞り、
|
|
428
437
|
その中でもう一度確かめてから消すので、確かめた後で別のlockに入れ替わらない。消す役の印そのものが残った時だけは自動で消さず、`pty_close`を案内する。
|
|
429
438
|
終了した持ち主のpidを別のprocessが使い回すと、pidだけでは生きて見える。生きて見える持ち主は、processが始まってからの経過とlockを作ってからの時間を比べ、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.61.
|
|
3
|
+
"version": "0.61.3",
|
|
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": [
|