aiterm-mcp 0.61.2 → 0.61.4
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 +17 -2
- package/README.ja.md +3 -2
- package/README.md +3 -2
- package/dist/core.js +7 -2
- package/dist/execution-dispatch.js +16 -5
- package/dist/execution-effect.js +30 -5
- package/dist/execution-runtime.js +5 -3
- package/dist/execution-service.js +89 -9
- package/dist/execution-tools.js +10 -5
- package/dist/tmux-runtime.js +12 -3
- package/docs/DESIGN.md +13 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.4] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- POSIXのnative実行口で最初の`agent_launch`がtmuxソケット未作成時に`EXECUTION_UNSUPPORTED`で止まっていた。発行直前にも空状態を確かめ、最初の`new-session`自身が作ったserverのPID・開始識別子とsession IDを実receiptから照合して起動する。補助sessionや入力再送は作らず、読取不能・receipt不一致は未確認で止める。
|
|
15
|
+
|
|
16
|
+
## [0.61.3] - 2026-10-22
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
10
23
|
## [0.61.2] - 2026-10-22
|
|
11
24
|
|
|
12
25
|
### 修正
|
|
13
26
|
|
|
14
|
-
- tmuxがまだ起動しておらず、対象ソケットが存在しない時、新規実行の登録が`EXECUTION_UNCONFIRMED
|
|
27
|
+
- tmuxがまだ起動しておらず、対象ソケットが存在しない時、新規実行の登録が`EXECUTION_UNCONFIRMED`で止まっていた。実行世代の観測でもソケット未作成を空状態として扱う。別ソケットや権限・接続の異常は引き続き不明として止める。この版では、その次の初回席作成が既存serverを要求しており、冷間起動はまだ完了しなかった。
|
|
15
28
|
|
|
16
29
|
## [0.61.1] - 2026-10-22
|
|
17
30
|
|
|
@@ -2715,7 +2728,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2715
2728
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2716
2729
|
provenance.
|
|
2717
2730
|
|
|
2718
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2731
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...HEAD
|
|
2732
|
+
[0.61.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.3...v0.61.4
|
|
2733
|
+
[0.61.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.2...v0.61.3
|
|
2719
2734
|
[0.61.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.1...v0.61.2
|
|
2720
2735
|
[0.61.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.0...v0.61.1
|
|
2721
2736
|
[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.4** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -731,11 +731,12 @@ 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
|
|
|
738
738
|
セッションは共有tmux socket(Windows nativeはpsmux namespace)上にある。`pty_open`/`agent_launch`の戻り値に表示されるattachコマンドで、人間がClaude/Codex/Grok/Cursor harnessの同じ端末へ入り、途中でキーボードを引き取れる。
|
|
739
|
+
POSIXでは、tmuxのソケットがまだ無くても最初の`agent_launch`でserverを起動できる。Aitermは作成したsessionとserverを実際のreceiptから照合して受付を確定し、不明な結果を入力の再送で補わない。
|
|
739
740
|
|
|
740
741
|
## 要件
|
|
741
742
|
|
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.4** · 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,11 +789,12 @@ 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
|
|
|
796
796
|
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.
|
|
797
|
+
On POSIX, the first `agent_launch` can create the tmux server when its socket does not yet exist. Aiterm verifies the exact created session and server before accepting that launch; an uncertain result is reported without retrying the input.
|
|
797
798
|
|
|
798
799
|
## Requirements
|
|
799
800
|
|
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
|
@@ -197,10 +197,13 @@ export function prepareExecutionCreation() {
|
|
|
197
197
|
refuse(scope, "EXECUTION_UNSUPPORTED");
|
|
198
198
|
try {
|
|
199
199
|
const server = scope.creation.prepare();
|
|
200
|
-
if (!Number.isSafeInteger(server?.pid) || server.pid < 1
|
|
200
|
+
if (server !== null && (!Number.isSafeInteger(server?.pid) || server.pid < 1
|
|
201
|
+
|| typeof server.started_identity !== "string" || !server.started_identity))
|
|
201
202
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
202
|
-
|
|
203
|
+
if (server)
|
|
204
|
+
scope.creationServer = { ...server };
|
|
203
205
|
scope.creationPrepared = true;
|
|
206
|
+
return server === null;
|
|
204
207
|
}
|
|
205
208
|
catch (error) {
|
|
206
209
|
scope.failure = new ExecutionError(error instanceof ExecutionError ? error.code : "EXECUTION_UNCONFIRMED", "not_started");
|
|
@@ -208,7 +211,7 @@ export function prepareExecutionCreation() {
|
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
/** Private core metadata stamp, derived only from this frame's actual create
|
|
211
|
-
* receipt and
|
|
214
|
+
* receipt and verified native server proof. Not a tool input or name lookup.
|
|
212
215
|
*/
|
|
213
216
|
export function executionCreatedNativeBinding(session) {
|
|
214
217
|
const scope = scopes.getStore();
|
|
@@ -237,7 +240,14 @@ export function captureExecutionCreatedNative(receipt) {
|
|
|
237
240
|
if (!parsed || parsed[2] !== scope.session || (scope.createdNative && scope.createdNative !== parsed[1]))
|
|
238
241
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
239
242
|
try {
|
|
240
|
-
scope.creation.native(scope.session, parsed[1], Number(parsed[3]));
|
|
243
|
+
const server = scope.creation.native(scope.session, parsed[1], Number(parsed[3]));
|
|
244
|
+
if (server) {
|
|
245
|
+
if (!Number.isSafeInteger(server.pid) || server.pid < 1 || typeof server.started_identity !== "string" || !server.started_identity)
|
|
246
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
247
|
+
scope.creationServer = { ...server };
|
|
248
|
+
}
|
|
249
|
+
if (!scope.creationServer)
|
|
250
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
241
251
|
}
|
|
242
252
|
catch (error) {
|
|
243
253
|
scope.failure = new ExecutionError(error instanceof ExecutionError ? error.code : "EXECUTION_UNCONFIRMED", "possible");
|
|
@@ -286,6 +296,21 @@ export function bindExecutionNativeTarget(session, nativeId) {
|
|
|
286
296
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
287
297
|
scope.nativeTargets.add(nativeId);
|
|
288
298
|
}
|
|
299
|
+
/** Bound only by the management-close wrapper, never tool arguments. The
|
|
300
|
+
* ordinary core close/cleanup keeps using its session name; native commands
|
|
301
|
+
* are pinned to the independently captured native ID through the same gate.
|
|
302
|
+
*/
|
|
303
|
+
export function bindExecutionManagementCloseTarget(session, nativeId) {
|
|
304
|
+
const scope = scopes.getStore();
|
|
305
|
+
if (!scope || scope.session !== session || scope.didIssue || scope.creation || scope.interrupt || !/^\$\d+$/.test(nativeId))
|
|
306
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
307
|
+
scope.managementCloseTarget = nativeId;
|
|
308
|
+
scope.nativeTargets.add(nativeId);
|
|
309
|
+
}
|
|
310
|
+
export function executionManagementCloseTarget(session) {
|
|
311
|
+
const scope = scopes.getStore();
|
|
312
|
+
return scope?.session === session ? scope.managementCloseTarget : undefined;
|
|
313
|
+
}
|
|
289
314
|
export function beforeExecutionEffect(session, verify) {
|
|
290
315
|
const scope = scopes.getStore();
|
|
291
316
|
if (!scope) {
|
|
@@ -376,7 +401,7 @@ export function guardTmuxExecution(input) {
|
|
|
376
401
|
if (!parsed || parsed[1] !== scope.session)
|
|
377
402
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
378
403
|
if (command !== "new-session")
|
|
379
|
-
args[at + 1] = `${scope.createdNative ?? "=" + scope.session}${parsed[2] ?? ":"}`;
|
|
404
|
+
args[at + 1] = `${scope.managementCloseTarget ?? scope.createdNative ?? "=" + scope.session}${parsed[2] ?? ":"}`;
|
|
380
405
|
}
|
|
381
406
|
effects = true;
|
|
382
407
|
}
|
|
@@ -84,11 +84,13 @@ export function createExecutionRuntime(bindings) {
|
|
|
84
84
|
session_id: executionLaunchSchema.shape.session_id, native_id: z.string().regex(/^\$\d+$/),
|
|
85
85
|
}).strict()) }).strict().parse(bindings.observeNativeSessionsSync());
|
|
86
86
|
if (new Set(parsed.sessions.map(value => value.session_id)).size !== parsed.sessions.length
|
|
87
|
-
|| new Set(parsed.sessions.map(value => value.native_id)).size !== parsed.sessions.length
|
|
87
|
+
|| new Set(parsed.sessions.map(value => value.native_id)).size !== parsed.sessions.length
|
|
88
|
+
|| (parsed.server === null && parsed.sessions.length > 0))
|
|
88
89
|
return { state: "unknown" };
|
|
90
|
+
const server = parsed.server === null ? { server: null } : parsed.server ? { server: parsed.server } : {};
|
|
89
91
|
const target = parsed.sessions.find(value => value.session_id === session);
|
|
90
|
-
return target ? { state: "present", native_id: target.native_id, ...
|
|
91
|
-
: { state: "absent", ...
|
|
92
|
+
return target ? { state: "present", native_id: target.native_id, ...server }
|
|
93
|
+
: { state: "absent", ...server };
|
|
92
94
|
}
|
|
93
95
|
catch {
|
|
94
96
|
return { state: "unknown" };
|
|
@@ -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)
|
|
@@ -384,18 +408,40 @@ export class ExecutionService {
|
|
|
384
408
|
if (record.operations.launch !== "preparing" || record.relation.lifecycle === "retired")
|
|
385
409
|
throw new ExecutionError("EXECUTION_CONFLICT");
|
|
386
410
|
const native = this.runtime.nativeSessionSync(record.reservation.target.session_id);
|
|
387
|
-
if (native.state !== "absent" ||
|
|
411
|
+
if (native.state !== "absent" || native.server === undefined)
|
|
388
412
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
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)
|
|
395
441
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
396
442
|
this.store.transactionSync(state => {
|
|
397
443
|
const record = find(state, request), native = this.runtime.nativeSessionSync?.(record.reservation.target.session_id);
|
|
398
|
-
if (
|
|
444
|
+
if (native?.state !== "absent" || !same(native.server, expected))
|
|
399
445
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
400
446
|
}, { readOnly: true });
|
|
401
447
|
}
|
|
@@ -403,18 +449,23 @@ export class ExecutionService {
|
|
|
403
449
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
404
450
|
if (!/^\$\d+$/.test(nativeId) || !this.store.transactionSync)
|
|
405
451
|
throw new ExecutionError("EXECUTION_UNSUPPORTED", "possible");
|
|
406
|
-
this.store.transactionSync(state => {
|
|
452
|
+
return this.store.transactionSync(state => {
|
|
407
453
|
const record = find(state, request), slot = state.slots.find(value => same(value.request, request) && value.stage === "launch");
|
|
454
|
+
// A cold first new-session creates the server itself. Prove its owner
|
|
455
|
+
// from the exact receipt and a stable native listing before stamping it.
|
|
456
|
+
const observed = expectedServer ? null : this.runtime.nativeSessionSync?.(session);
|
|
457
|
+
const server = expectedServer ?? (observed?.state === "present" && observed.native_id === nativeId ? observed.server : undefined);
|
|
408
458
|
if (!slot || slot.state !== "issued" || record.operations.launch !== "issued" || slot.session_id !== session
|
|
409
459
|
|| record.reservation.target.expected_launch_id !== null || record.relation.lifecycle === "retired"
|
|
410
|
-
||
|
|
460
|
+
|| !server || server.pid !== receiptPid
|
|
411
461
|
|| (slot.created_native_id && slot.created_native_id !== nativeId)
|
|
412
|
-
|| (slot.created_server && !same(slot.created_server,
|
|
462
|
+
|| (slot.created_server && !same(slot.created_server, server)))
|
|
413
463
|
throw new ExecutionError("EXECUTION_CONFLICT", "possible");
|
|
414
464
|
// Preserve this genuine native receipt even if a stop is already fencing
|
|
415
465
|
// its caller. Binding metadata does not authorize another native action.
|
|
416
466
|
slot.created_native_id = nativeId;
|
|
417
|
-
slot.created_server =
|
|
467
|
+
slot.created_server = server;
|
|
468
|
+
return server;
|
|
418
469
|
});
|
|
419
470
|
}
|
|
420
471
|
bindCreatedLaunch(metaInput, input) {
|
|
@@ -564,7 +615,7 @@ export class ExecutionService {
|
|
|
564
615
|
}
|
|
565
616
|
sourceAdmission(state, source.point, args.source_session_id);
|
|
566
617
|
const target = args.reservation.target;
|
|
567
|
-
if (state.
|
|
618
|
+
if (targetBlocked(state, args.reservation.request, target.session_id, target.expected_launch_id))
|
|
568
619
|
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
569
620
|
const current = await this.runtime.observe(target.session_id);
|
|
570
621
|
this.expectTarget(current, target.expected_launch_id, target.session_id);
|
|
@@ -606,6 +657,7 @@ export class ExecutionService {
|
|
|
606
657
|
}
|
|
607
658
|
async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission) {
|
|
608
659
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
660
|
+
let expectedCloseNative;
|
|
609
661
|
if (deferred && (!this.store.transactionSync || !this.runtime.observeSync))
|
|
610
662
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
611
663
|
await this.store.transaction(async (state) => {
|
|
@@ -625,6 +677,16 @@ export class ExecutionService {
|
|
|
625
677
|
if (record.reservation.source.launch && !relation.source)
|
|
626
678
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
627
679
|
if (deferred) {
|
|
680
|
+
const target = record.reservation.target;
|
|
681
|
+
if (recoverableSends(state, request, target.session_id, target.expected_launch_id).length) {
|
|
682
|
+
if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
683
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
684
|
+
this.expectTarget(await this.runtime.observe(target.session_id), target.expected_launch_id, target.session_id);
|
|
685
|
+
const native = this.runtime.nativeSessionSync?.(target.session_id);
|
|
686
|
+
if (native?.state !== "present" || !native.server)
|
|
687
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
688
|
+
expectedCloseNative = { native_id: native.native_id, server: structuredClone(native.server) };
|
|
689
|
+
}
|
|
628
690
|
record.operations[stage] = "preparing";
|
|
629
691
|
return; // no native effect or target slot yet; stop can cancel this
|
|
630
692
|
}
|
|
@@ -654,6 +716,14 @@ export class ExecutionService {
|
|
|
654
716
|
state.slots.push({ session_id: target.session_id, expected_launch_id: expected, stage, state: "issued", request });
|
|
655
717
|
});
|
|
656
718
|
let issued = !deferred, finished = false;
|
|
719
|
+
const checkCloseNative = (session) => {
|
|
720
|
+
if (!expectedCloseNative)
|
|
721
|
+
return;
|
|
722
|
+
const native = this.runtime.nativeSessionSync?.(session);
|
|
723
|
+
if (native?.state !== "present" || native.native_id !== expectedCloseNative.native_id
|
|
724
|
+
|| !same(native.server, expectedCloseNative.server))
|
|
725
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
726
|
+
};
|
|
657
727
|
const checkOrIssue = (commit, preparation) => {
|
|
658
728
|
admission?.();
|
|
659
729
|
const prepared = () => { admission?.(); return preparation(); };
|
|
@@ -688,6 +758,7 @@ export class ExecutionService {
|
|
|
688
758
|
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
689
759
|
}
|
|
690
760
|
const current = this.runtime.observeSync(slot.session_id);
|
|
761
|
+
checkCloseNative(slot.session_id);
|
|
691
762
|
if (current.state === "present" && stopped(state, current.launch))
|
|
692
763
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
693
764
|
if (slot.created_native_id) {
|
|
@@ -725,11 +796,12 @@ export class ExecutionService {
|
|
|
725
796
|
throw new ExecutionError("EXECUTION_STALE_GENERATION");
|
|
726
797
|
}
|
|
727
798
|
const target = record.reservation.target;
|
|
728
|
-
if (state
|
|
799
|
+
if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
|
|
729
800
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
730
801
|
const expected = relation.target?.launch.launch_id ?? target.expected_launch_id;
|
|
731
802
|
const current = this.runtime.observeSync(target.session_id);
|
|
732
803
|
this.expectTarget(current, expected, target.session_id);
|
|
804
|
+
checkCloseNative(target.session_id);
|
|
733
805
|
if (current.state === "present" && stopped(state, current.launch))
|
|
734
806
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
735
807
|
if ((stage === "launch" && current.state !== "absent") || (stage === "send" && current.state !== "present"))
|
|
@@ -830,8 +902,14 @@ export class ExecutionService {
|
|
|
830
902
|
if (state.stopping.some(value => same(value, record.relation.source?.launch)) && !state.stopping.some(value => same(value, targetPoint.launch)))
|
|
831
903
|
state.stopping.push(targetPoint.launch);
|
|
832
904
|
state.slots = state.slots.filter(value => !same(value.request, request));
|
|
833
|
-
if (closing)
|
|
905
|
+
if (closing) {
|
|
906
|
+
// Actual management close plus confirmed absence releases only these
|
|
907
|
+
// failed send admissions. Old callbacks still fail their missing-slot
|
|
908
|
+
// guard, and their records remain unknown and cannot be invoked again.
|
|
909
|
+
const recovered = recoverableSends(state, request, slot.session_id, expected);
|
|
910
|
+
state.slots = state.slots.filter(value => !recovered.includes(value));
|
|
834
911
|
this.interruptRelations(state, targetPoint.launch);
|
|
912
|
+
}
|
|
835
913
|
return view(state, record);
|
|
836
914
|
}).catch(async () => {
|
|
837
915
|
// The side effect already returned, but its result could not be safely
|
|
@@ -1326,6 +1404,8 @@ export class ExecutionService {
|
|
|
1326
1404
|
interruptRelations(state, launch) {
|
|
1327
1405
|
for (const record of state.records)
|
|
1328
1406
|
if (same(record.relation.target?.launch, launch)) {
|
|
1407
|
+
if (record.operations.launch === "unknown" || record.operations.send === "unknown")
|
|
1408
|
+
continue;
|
|
1329
1409
|
record.relation.completion = "interrupted";
|
|
1330
1410
|
record.relation.target_fence = "closed";
|
|
1331
1411
|
}
|
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,14 +119,19 @@ 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
|
-
verifyServer: () => { if (
|
|
129
|
+
verifyServer: () => { if (expectedServer === undefined)
|
|
125
130
|
throw new ExecutionError("EXECUTION_UNSUPPORTED"); service.verifyCreationServer(ref, expectedServer); },
|
|
126
131
|
native: (session, nativeId, serverPid) => {
|
|
127
|
-
if (
|
|
132
|
+
if (expectedServer === undefined)
|
|
128
133
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
129
|
-
service.captureCreatedNative(ref, session, nativeId, expectedServer, serverPid);
|
|
134
|
+
return service.captureCreatedNative(ref, session, nativeId, expectedServer, serverPid);
|
|
130
135
|
},
|
|
131
136
|
launch: launch => service.bindCreatedLaunch(ref, launch),
|
|
132
137
|
} : undefined, prepare);
|
package/dist/tmux-runtime.js
CHANGED
|
@@ -493,12 +493,12 @@ export function registeredSessionEnvironment(name) {
|
|
|
493
493
|
export function tmuxNewSession(observe, name, launch, registered = []) {
|
|
494
494
|
const guarded = hasExecutionCreationScope();
|
|
495
495
|
// Windows per-session server creation needs a separate native-owner proof.
|
|
496
|
-
// The legacy public launcher is unchanged
|
|
497
|
-
|
|
496
|
+
// The legacy public launcher is unchanged.
|
|
497
|
+
let cold = false;
|
|
498
498
|
if (guarded) {
|
|
499
499
|
if (isWin)
|
|
500
500
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
501
|
-
prepareExecutionCreation();
|
|
501
|
+
cold = prepareExecutionCreation();
|
|
502
502
|
}
|
|
503
503
|
const printNative = guarded ? ["-P", "-F", "#{session_id}\t#{session_name}\t#{pid}"] : [];
|
|
504
504
|
if (isWin) {
|
|
@@ -508,6 +508,15 @@ export function tmuxNewSession(observe, name, launch, registered = []) {
|
|
|
508
508
|
return result;
|
|
509
509
|
}
|
|
510
510
|
const env = sessionCallerEnvironment();
|
|
511
|
+
if (cold) {
|
|
512
|
+
// With no server, there is no global environment to adjust. The first
|
|
513
|
+
// new-session inherits this caller's environment and prints its native ID
|
|
514
|
+
// and newly created server PID for the guarded receipt proof.
|
|
515
|
+
const r = runTmux(observe, undefined, ["new-session", "-d", "-s", name, ...printNative, ...launch.args, launch.shell], env);
|
|
516
|
+
if (r.code === 0 || r.stdout)
|
|
517
|
+
captureExecutionCreatedNative(r.stdout);
|
|
518
|
+
return r;
|
|
519
|
+
}
|
|
511
520
|
const wildcard = tmuxHasSessionEnvironmentFlags();
|
|
512
521
|
// tmux は起動時に PWD を共通環境へ足す。
|
|
513
522
|
const others = new Set(["PWD"]);
|
package/docs/DESIGN.md
CHANGED
|
@@ -28,8 +28,10 @@ native emit直前に照合したhook参照は既存deliveryへ保存する。Cod
|
|
|
28
28
|
POSIXのraw tty回復もnative作用であり、tmux打鍵と同じ受付を通す。ttyは正確なsession selectorで読み、
|
|
29
29
|
実stty直前にも同じdeviceを確認する。停止や世代/tty交代後に古いdeviceへ回復を発行しない。
|
|
30
30
|
一つのRPCが複数のnative作用を発行するときも、各回に同じissued slotとsource/target世代・停止を再確認する。
|
|
31
|
-
新launchは、既存POSIX serverのnative owner
|
|
32
|
-
|
|
31
|
+
新launchは、既存POSIX serverのnative owner、またはserver未作成の完全な空観測を受付時と発行直前に確認する。
|
|
32
|
+
冷間時は最初のnew-session自身でserverを作り、その実receiptのnative IDとserver PIDを、作成後の安定したnative一覧とprocess開始識別子で照合する。
|
|
33
|
+
既存server時も冷間時も、作成したsessionのnative ID・server owner・core内部のlaunch世代を同じ要求slotへ段階結合する。
|
|
34
|
+
物理shell準備はnative IDへ固定し、未結合の同名世代を採用しない。冷間起動のための補助sessionや再送は作らない。
|
|
33
35
|
作成時のnative ID/server ownerをmanaged metadataへ保存し、private runtimeの世代読取と期待closeでも再照合する。
|
|
34
36
|
古いmetadataの欠測を現観測で補わず、既存席の自動登録は行わない。
|
|
35
37
|
証拠の無い新server起動、Windows固有のserver作成、auto-name/通常PTY/auth等はまだunsupported。
|
|
@@ -424,6 +426,15 @@ ADR 0074(repositoryの`docs/adr/0074-agent-model-catalog.md`)。
|
|
|
424
426
|
境界失敗は明示errorにする。retry、別model、別harness、別backendへ自動fallbackしない。
|
|
425
427
|
stale send lockは並行processとのABAを避けるため自動削除せず、公開APIでは対象sessionを`pty_close`して
|
|
426
428
|
同じIDで再作成する。全session一括停止は公開しない。
|
|
429
|
+
POSIXの明示native execution構成では、返却済みsendのunknown枠が対象席の管理closeまで
|
|
430
|
+
永久に拒否しないよう、通常`pty_close`だけを同じ期待launch/native IDで受付できる。
|
|
431
|
+
unknown send枠を保持したまま既存core closeへ進み、native close成功と席の不在を確認した後だけ
|
|
432
|
+
該当send枠を解放する。元recordのphase/completion/sendはunknownのまま残し、再送や成功化をしない。
|
|
433
|
+
旧callbackは未知/消失した元slotで拒否され、同IDの新launchへnative作用を発行できない。
|
|
434
|
+
issued、管理close自体のunknown、親作用のslot、世代/native/serverの不明・交代は対象外。
|
|
435
|
+
既存の送信・待機lockの拒否も維持し、他席やissuerの停止を復旧条件へ追加しない。
|
|
436
|
+
native handlerのisErrorは元の本文を保持し、`aiterm/execution_error`のcode/effectを併記する。
|
|
437
|
+
native作用後の不明はpossibleのままで、原因説明を再送可否や成功receiptへ変換しない。
|
|
427
438
|
agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が長いので、持ち主が終了した残骸を次の送信が片付ける。消す役を`<name>.agent-send.lock.reap`の排他作成で1つに絞り、
|
|
428
439
|
その中でもう一度確かめてから消すので、確かめた後で別のlockに入れ替わらない。消す役の印そのものが残った時だけは自動で消さず、`pty_close`を案内する。
|
|
429
440
|
終了した持ち主の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.4",
|
|
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": [
|