aiterm-mcp 0.61.3 → 0.61.5
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 +18 -2
- package/README.ja.md +4 -1
- package/README.md +4 -1
- package/dist/core.js +30 -28
- package/dist/execution-dispatch.js +5 -2
- package/dist/execution-effect.js +47 -6
- package/dist/execution-runtime.js +5 -3
- package/dist/execution-service.js +36 -8
- package/dist/execution-store.js +44 -1
- package/dist/execution-tools.js +4 -4
- package/dist/tmux-runtime.js +12 -3
- package/docs/DESIGN.md +11 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.5] - 2026-10-22
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- In the opt-in POSIX native entry, an ordinary incoming send to a verified caller no longer invalidates that caller's outgoing send between paste and Enter. The incoming slot remains owned by its original request; unknown input, management operations, stops and generation changes remain blockers. Reciprocal sends do not wait on each other.
|
|
15
|
+
- Agent paste/submit segments asynchronously acquire the existing metadata mutex before running their synchronous native gates. Routine graph-writer contention no longer poisons an already pasted input. Each effect still rechecks generations and stop state, and issuance is durable before input; no native command is replayed.
|
|
16
|
+
- BellTeam's explicit promptless management `agent_launch` now uses the serving process/request identity after `pty_close`. Unreferenced prompt delivery, portable forks and automatic names remain refused.
|
|
17
|
+
|
|
18
|
+
## [0.61.4] - 2026-10-22
|
|
19
|
+
|
|
20
|
+
### 修正
|
|
21
|
+
|
|
22
|
+
- POSIXのnative実行口で最初の`agent_launch`がtmuxソケット未作成時に`EXECUTION_UNSUPPORTED`で止まっていた。発行直前にも空状態を確かめ、最初の`new-session`自身が作ったserverのPID・開始識別子とsession IDを実receiptから照合して起動する。補助sessionや入力再送は作らず、読取不能・receipt不一致は未確認で止める。
|
|
23
|
+
|
|
10
24
|
## [0.61.3] - 2026-10-22
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
@@ -18,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
32
|
|
|
19
33
|
### 修正
|
|
20
34
|
|
|
21
|
-
- tmuxがまだ起動しておらず、対象ソケットが存在しない時、新規実行の登録が`EXECUTION_UNCONFIRMED
|
|
35
|
+
- tmuxがまだ起動しておらず、対象ソケットが存在しない時、新規実行の登録が`EXECUTION_UNCONFIRMED`で止まっていた。実行世代の観測でもソケット未作成を空状態として扱う。別ソケットや権限・接続の異常は引き続き不明として止める。この版では、その次の初回席作成が既存serverを要求しており、冷間起動はまだ完了しなかった。
|
|
22
36
|
|
|
23
37
|
## [0.61.1] - 2026-10-22
|
|
24
38
|
|
|
@@ -2722,7 +2736,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2722
2736
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2723
2737
|
provenance.
|
|
2724
2738
|
|
|
2725
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2739
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...HEAD
|
|
2740
|
+
[0.61.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...v0.61.5
|
|
2741
|
+
[0.61.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.3...v0.61.4
|
|
2726
2742
|
[0.61.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.2...v0.61.3
|
|
2727
2743
|
[0.61.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.1...v0.61.2
|
|
2728
2744
|
[0.61.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.0...v0.61.1
|
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.5** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -735,7 +735,10 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
|
|
|
735
735
|
|
|
736
736
|
## 人が覗く
|
|
737
737
|
|
|
738
|
+
POSIXの明示native実行構成では、検証済みcallerへ通常メッセージが届く間も、そのcallerから別席への送信を継続できる。callerの世代や各requestのslotは書き換えない。agentの貼付け・Enterはmetadata mutexの競合を入力前に待ち、不明な操作・世代交代・停止では入力を止める。BellTeamは閉じた席をpromptなしの明示管理`agent_launch`で再作成できる。promptを送る呼出しには元の実行参照が必要である。
|
|
739
|
+
|
|
738
740
|
セッションは共有tmux socket(Windows nativeはpsmux namespace)上にある。`pty_open`/`agent_launch`の戻り値に表示されるattachコマンドで、人間がClaude/Codex/Grok/Cursor harnessの同じ端末へ入り、途中でキーボードを引き取れる。
|
|
741
|
+
POSIXでは、tmuxのソケットがまだ無くても最初の`agent_launch`でserverを起動できる。Aitermは作成したsessionとserverを実際のreceiptから照合して受付を確定し、不明な結果を入力の再送で補わない。
|
|
739
742
|
|
|
740
743
|
## 要件
|
|
741
744
|
|
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.5** · 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
|
|
|
@@ -793,7 +793,10 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
|
|
|
793
793
|
|
|
794
794
|
## A human can watch
|
|
795
795
|
|
|
796
|
+
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.
|
|
797
|
+
|
|
796
798
|
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.
|
|
799
|
+
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
800
|
|
|
798
801
|
## Requirements
|
|
799
802
|
|
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, executionManagementCloseTarget } from "./execution-effect.js";
|
|
19
|
+
import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary, executionManagementCloseTarget, withExecutionNativeAdmission } 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";
|
|
@@ -5792,29 +5792,31 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
5792
5792
|
prepareSendText(dispatchText, { raw: o.raw });
|
|
5793
5793
|
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
5794
5794
|
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5795
|
+
await withExecutionNativeAdmission(name, () => {
|
|
5796
|
+
captureExecutionSendBoundary({ launch: { session_id: name, launch_id: meta.launch_id }, boundary: {
|
|
5797
|
+
event_cursor: startOffset, operation_id: operationId, turn_id: null, steer_id: null
|
|
5798
|
+
} });
|
|
5799
|
+
recordInterimBoundary(meta, startOffset, operationId);
|
|
5800
|
+
if (meta.kind === "claude") {
|
|
5801
|
+
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
5802
|
+
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
5803
|
+
prepareSendText(text, { raw: o.raw });
|
|
5804
|
+
if (operationId) {
|
|
5805
|
+
reserveClaudeOperation(meta, operationId);
|
|
5806
|
+
}
|
|
5807
|
+
else
|
|
5808
|
+
reserveAnonymousClaudeTurn(meta);
|
|
5805
5809
|
}
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
bracketedPaste: true,
|
|
5817
|
-
typedPreface: prefaced.typedPreface,
|
|
5810
|
+
send(name, dispatchText, {
|
|
5811
|
+
enter: false,
|
|
5812
|
+
force: o.force,
|
|
5813
|
+
raw: o.raw,
|
|
5814
|
+
mark: false,
|
|
5815
|
+
rtk: false,
|
|
5816
|
+
preserveAgentOperation: meta.kind === "claude",
|
|
5817
|
+
bracketedPaste: true,
|
|
5818
|
+
typedPreface: prefaced.typedPreface,
|
|
5819
|
+
});
|
|
5818
5820
|
});
|
|
5819
5821
|
// Cursorの冷間起動ではbracketed pasteの反映が250msを超えることがある。本文がcomposerへ
|
|
5820
5822
|
// 現れた実測をsubmit条件にし、未反映のままEnterだけを失う競合を作らない。
|
|
@@ -5829,7 +5831,7 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
5829
5831
|
// Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
|
|
5830
5832
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
5831
5833
|
}
|
|
5832
|
-
sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
|
|
5834
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" }));
|
|
5833
5835
|
let residue = await detectAgentSubmitResidue(name, meta.kind, dispatchText);
|
|
5834
5836
|
// Cursorはcomposerへの貼付反映後でも最初のEnterを取り落とすことがある。本文残留を
|
|
5835
5837
|
// 陽性観測した場合だけ同じEnterを一度再送し、再検査後も残る時は失敗として返す。
|
|
@@ -6029,7 +6031,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
6029
6031
|
};
|
|
6030
6032
|
// Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
|
|
6031
6033
|
const preserveAgentOperation = meta.kind === "claude";
|
|
6032
|
-
send(name, text, {
|
|
6034
|
+
await withExecutionNativeAdmission(name, () => send(name, text, {
|
|
6033
6035
|
enter: false,
|
|
6034
6036
|
force: true,
|
|
6035
6037
|
raw: o.raw,
|
|
@@ -6038,7 +6040,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
6038
6040
|
preserveAgentOperation,
|
|
6039
6041
|
bracketedPaste: true,
|
|
6040
6042
|
typedPreface: prefaced.typedPreface,
|
|
6041
|
-
});
|
|
6043
|
+
}));
|
|
6042
6044
|
if (meta.kind === "cursor") {
|
|
6043
6045
|
const visible = await waitCursorPromptVisible(name, text);
|
|
6044
6046
|
if (!visible.visible) {
|
|
@@ -6048,7 +6050,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
6048
6050
|
else {
|
|
6049
6051
|
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
6050
6052
|
}
|
|
6051
|
-
sendKey(name, "Enter", { preserveAgentOperation });
|
|
6053
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, "Enter", { preserveAgentOperation }));
|
|
6052
6054
|
// GrokとCursorは実行中の送信を待ち行列へ入れる。そのままだと現在turnの完了後に別turnとして動き、
|
|
6053
6055
|
// 完了通知が差し込み前の回答で届いてしまう。待ち行列へ入ったことを確かめ、標準の「今すぐ送る」で現在turnへ移す。
|
|
6054
6056
|
const queued = meta.kind === "grok" ? grokSteerQueued
|
|
@@ -6064,7 +6066,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
6064
6066
|
`差し込む文が${label}の待ち行列へ入ったことを確認できません。子のturnが送る直前に終わっていた時は、` +
|
|
6065
6067
|
`文は新しいturnとして始まっており、その完了は通知されません。再送する前にpty_read(screen:true)で確かめてください。`, 2);
|
|
6066
6068
|
}
|
|
6067
|
-
sendKey(name, "Enter");
|
|
6069
|
+
await withExecutionNativeAdmission(name, () => sendKey(name, "Enter"));
|
|
6068
6070
|
if (!await waitSteerScreen(name, screen => !queued(screen), stillRunning)) {
|
|
6069
6071
|
throw new AitermError(`STEER_STILL_QUEUED vendor=${meta.kind} session=${name}\n` +
|
|
6070
6072
|
`差し込む文が${label}の待ち行列に残っています。現在のturnが終わると別turnとして実行されます。`, 2);
|
|
@@ -143,12 +143,15 @@ export function createExecutionDispatcher(bindings) {
|
|
|
143
143
|
// A host's explicit session close is a management RPC with its own
|
|
144
144
|
// existing process/request identity, not a delivery or resend.
|
|
145
145
|
const hostClose = client === "bellteam" && route.tool === "pty_close" && !supplied;
|
|
146
|
+
const hostLaunch = client === "bellteam" && route.tool === "agent_launch" && !supplied
|
|
147
|
+
&& args.prompt == null && args.throughline_source_session === undefined;
|
|
148
|
+
const hostManagement = hostClose || hostLaunch;
|
|
146
149
|
if (args.remote !== undefined && args.remote !== null)
|
|
147
150
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
148
151
|
let reference = supplied;
|
|
149
152
|
let directRequest = null;
|
|
150
153
|
let frozenParent = null;
|
|
151
|
-
if (!reference && (client !== "bellteam" ||
|
|
154
|
+
if (!reference && (client !== "bellteam" || hostManagement) && native) {
|
|
152
155
|
directRequest = executionRequestSchema.options[4].parse({ kind: "mcp", process_pid: native.pid,
|
|
153
156
|
process_started_identity: native.started_identity, request_id: fixedExtra.requestId,
|
|
154
157
|
tool: route.tool, target_session_id: route.target });
|
|
@@ -189,7 +192,7 @@ export function createExecutionDispatcher(bindings) {
|
|
|
189
192
|
}
|
|
190
193
|
else {
|
|
191
194
|
// BellTeam must retain its existing delivery/room/call reference.
|
|
192
|
-
if ((client === "bellteam" && !
|
|
195
|
+
if ((client === "bellteam" && !hostManagement) || !directRequest)
|
|
193
196
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
194
197
|
const current = await observe(route.target);
|
|
195
198
|
if (route.stage === "send" && !current.point)
|
package/dist/execution-effect.js
CHANGED
|
@@ -106,15 +106,46 @@ function prepareInScope(scope, body) {
|
|
|
106
106
|
throw scope.failure;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
export function withExecutionEffectScope(session, issue, body, creation, prepare) {
|
|
109
|
+
export function withExecutionEffectScope(session, issue, body, creation, prepare, admitNative) {
|
|
110
110
|
const enclosing = scopes.getStore();
|
|
111
111
|
if (enclosing?.interrupt)
|
|
112
112
|
refuse(enclosing, "EXECUTION_UNSUPPORTED");
|
|
113
113
|
const target = executionLaunchSchema.shape.session_id.parse(session);
|
|
114
|
-
const scope = { session: target, issue, prepare, nativeTargets: new Set(), didIssue: false,
|
|
114
|
+
const scope = { session: target, issue, prepare, admitNative, nativeTargets: new Set(), didIssue: false,
|
|
115
115
|
...(creation ? { creation: { ...creation } } : {}) };
|
|
116
116
|
return runEffectScope(scope, body);
|
|
117
117
|
}
|
|
118
|
+
/** Resume one synchronous native segment after asynchronous readiness/waits.
|
|
119
|
+
* Waits for the existing metadata mutex, never replays the native body. The
|
|
120
|
+
* normal gates inside body still check source/target/stop and persist issuance.
|
|
121
|
+
*/
|
|
122
|
+
export async function withExecutionNativeAdmission(session, body) {
|
|
123
|
+
const scope = scopes.getStore();
|
|
124
|
+
if (!scope) {
|
|
125
|
+
if (required)
|
|
126
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
127
|
+
return body();
|
|
128
|
+
}
|
|
129
|
+
if (scope.session !== session)
|
|
130
|
+
refuse(scope, "EXECUTION_CONFLICT");
|
|
131
|
+
if (scope.failure)
|
|
132
|
+
throw scope.failure;
|
|
133
|
+
if (!scope.admitNative)
|
|
134
|
+
return body(); // explicit legacy/fake frame
|
|
135
|
+
try {
|
|
136
|
+
return await scope.admitNative(() => {
|
|
137
|
+
if (scope.failure)
|
|
138
|
+
throw scope.failure;
|
|
139
|
+
assertExecutionContext(session);
|
|
140
|
+
return body();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
scope.failure ??= error instanceof ExecutionError ? new ExecutionError(error.code, scope.didIssue ? "possible" : error.effect)
|
|
145
|
+
: new ExecutionError("EXECUTION_UNCONFIRMED", scope.didIssue ? "possible" : "not_started");
|
|
146
|
+
throw scope.failure;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
118
149
|
/** Private stop-owned synchronous frame over the existing native gate. It
|
|
119
150
|
* creates no invocation/edge/request. The ordinary stop path supplies only
|
|
120
151
|
* its target check, without a durable journal or completion/fence prerequisite.
|
|
@@ -197,10 +228,13 @@ export function prepareExecutionCreation() {
|
|
|
197
228
|
refuse(scope, "EXECUTION_UNSUPPORTED");
|
|
198
229
|
try {
|
|
199
230
|
const server = scope.creation.prepare();
|
|
200
|
-
if (!Number.isSafeInteger(server?.pid) || server.pid < 1
|
|
231
|
+
if (server !== null && (!Number.isSafeInteger(server?.pid) || server.pid < 1
|
|
232
|
+
|| typeof server.started_identity !== "string" || !server.started_identity))
|
|
201
233
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
202
|
-
|
|
234
|
+
if (server)
|
|
235
|
+
scope.creationServer = { ...server };
|
|
203
236
|
scope.creationPrepared = true;
|
|
237
|
+
return server === null;
|
|
204
238
|
}
|
|
205
239
|
catch (error) {
|
|
206
240
|
scope.failure = new ExecutionError(error instanceof ExecutionError ? error.code : "EXECUTION_UNCONFIRMED", "not_started");
|
|
@@ -208,7 +242,7 @@ export function prepareExecutionCreation() {
|
|
|
208
242
|
}
|
|
209
243
|
}
|
|
210
244
|
/** Private core metadata stamp, derived only from this frame's actual create
|
|
211
|
-
* receipt and
|
|
245
|
+
* receipt and verified native server proof. Not a tool input or name lookup.
|
|
212
246
|
*/
|
|
213
247
|
export function executionCreatedNativeBinding(session) {
|
|
214
248
|
const scope = scopes.getStore();
|
|
@@ -237,7 +271,14 @@ export function captureExecutionCreatedNative(receipt) {
|
|
|
237
271
|
if (!parsed || parsed[2] !== scope.session || (scope.createdNative && scope.createdNative !== parsed[1]))
|
|
238
272
|
refuse(scope, "EXECUTION_CONFLICT");
|
|
239
273
|
try {
|
|
240
|
-
scope.creation.native(scope.session, parsed[1], Number(parsed[3]));
|
|
274
|
+
const server = scope.creation.native(scope.session, parsed[1], Number(parsed[3]));
|
|
275
|
+
if (server) {
|
|
276
|
+
if (!Number.isSafeInteger(server.pid) || server.pid < 1 || typeof server.started_identity !== "string" || !server.started_identity)
|
|
277
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
278
|
+
scope.creationServer = { ...server };
|
|
279
|
+
}
|
|
280
|
+
if (!scope.creationServer)
|
|
281
|
+
throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
|
|
241
282
|
}
|
|
242
283
|
catch (error) {
|
|
243
284
|
scope.failure = new ExecutionError(error instanceof ExecutionError ? error.code : "EXECUTION_UNCONFIRMED", "possible");
|
|
@@ -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" };
|
|
@@ -70,11 +70,29 @@ function view(state, record) {
|
|
|
70
70
|
stopping: record.stopping || stopped(state, record.relation.source?.launch) || stopped(state, record.relation.target?.launch)
|
|
71
71
|
|| interruptionBlocksRequest(state, record.reservation.request) });
|
|
72
72
|
}
|
|
73
|
+
/** An admitted ordinary input to the caller does not replace its generation
|
|
74
|
+
* or revoke its outstanding outgoing RPC. Blocking that input's issued slot
|
|
75
|
+
* would make reciprocal sends poison each other between paste and Enter.
|
|
76
|
+
* Unknown operations and management/creation slots are never exempted.
|
|
77
|
+
*/
|
|
78
|
+
function blocksSource(state, source, seatHint, own) {
|
|
79
|
+
return state.slots.some(slot => {
|
|
80
|
+
if (slot === own || slot.session_id !== (source?.session_id ?? seatHint))
|
|
81
|
+
return false;
|
|
82
|
+
if (!source || slot.stage !== "send" || slot.state !== "issued" || slot.expected_launch_id !== source.launch_id || !slot.request
|
|
83
|
+
|| slot.request.kind === "mcp" && !["pty_send", "claude_turn.issue"].includes(slot.request.tool))
|
|
84
|
+
return true;
|
|
85
|
+
const incoming = state.records.find(value => same(value.reservation.request, slot.request));
|
|
86
|
+
return incoming?.operations.send !== "issued" || ["issued", "unknown"].includes(incoming.operations.launch)
|
|
87
|
+
|| incoming.relation.phase !== "submitted_unknown" || incoming.relation.completion !== "open"
|
|
88
|
+
|| incoming.relation.lifecycle !== "tracking" || !same(incoming.relation.target?.launch, source);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
73
91
|
function sourceAdmission(state, point, seatHint) {
|
|
74
92
|
assertInterruptionSource(state, point, seatHint);
|
|
75
93
|
if (stopped(state, point?.launch))
|
|
76
94
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
77
|
-
if (state
|
|
95
|
+
if (blocksSource(state, point?.launch ?? null, seatHint))
|
|
78
96
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
79
97
|
if (!point && seatHint && state.stopping.some(value => value.session_id === seatHint))
|
|
80
98
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
@@ -408,7 +426,7 @@ export class ExecutionService {
|
|
|
408
426
|
if (record.operations.launch !== "preparing" || record.relation.lifecycle === "retired")
|
|
409
427
|
throw new ExecutionError("EXECUTION_CONFLICT");
|
|
410
428
|
const native = this.runtime.nativeSessionSync(record.reservation.target.session_id);
|
|
411
|
-
if (native.state !== "absent" ||
|
|
429
|
+
if (native.state !== "absent" || native.server === undefined)
|
|
412
430
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
413
431
|
return native.server;
|
|
414
432
|
}, { readOnly: true });
|
|
@@ -441,7 +459,7 @@ export class ExecutionService {
|
|
|
441
459
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
442
460
|
this.store.transactionSync(state => {
|
|
443
461
|
const record = find(state, request), native = this.runtime.nativeSessionSync?.(record.reservation.target.session_id);
|
|
444
|
-
if (
|
|
462
|
+
if (native?.state !== "absent" || !same(native.server, expected))
|
|
445
463
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
446
464
|
}, { readOnly: true });
|
|
447
465
|
}
|
|
@@ -449,18 +467,23 @@ export class ExecutionService {
|
|
|
449
467
|
const { request } = executionRefMetaSchema.parse(metaInput);
|
|
450
468
|
if (!/^\$\d+$/.test(nativeId) || !this.store.transactionSync)
|
|
451
469
|
throw new ExecutionError("EXECUTION_UNSUPPORTED", "possible");
|
|
452
|
-
this.store.transactionSync(state => {
|
|
470
|
+
return this.store.transactionSync(state => {
|
|
453
471
|
const record = find(state, request), slot = state.slots.find(value => same(value.request, request) && value.stage === "launch");
|
|
472
|
+
// A cold first new-session creates the server itself. Prove its owner
|
|
473
|
+
// from the exact receipt and a stable native listing before stamping it.
|
|
474
|
+
const observed = expectedServer ? null : this.runtime.nativeSessionSync?.(session);
|
|
475
|
+
const server = expectedServer ?? (observed?.state === "present" && observed.native_id === nativeId ? observed.server : undefined);
|
|
454
476
|
if (!slot || slot.state !== "issued" || record.operations.launch !== "issued" || slot.session_id !== session
|
|
455
477
|
|| record.reservation.target.expected_launch_id !== null || record.relation.lifecycle === "retired"
|
|
456
|
-
||
|
|
478
|
+
|| !server || server.pid !== receiptPid
|
|
457
479
|
|| (slot.created_native_id && slot.created_native_id !== nativeId)
|
|
458
|
-
|| (slot.created_server && !same(slot.created_server,
|
|
480
|
+
|| (slot.created_server && !same(slot.created_server, server)))
|
|
459
481
|
throw new ExecutionError("EXECUTION_CONFLICT", "possible");
|
|
460
482
|
// Preserve this genuine native receipt even if a stop is already fencing
|
|
461
483
|
// its caller. Binding metadata does not authorize another native action.
|
|
462
484
|
slot.created_native_id = nativeId;
|
|
463
|
-
slot.created_server =
|
|
485
|
+
slot.created_server = server;
|
|
486
|
+
return server;
|
|
464
487
|
});
|
|
465
488
|
}
|
|
466
489
|
bindCreatedLaunch(metaInput, input) {
|
|
@@ -523,6 +546,11 @@ export class ExecutionService {
|
|
|
523
546
|
return this.store.transaction(state => view(state, find(state, args.request)), { readOnly: true });
|
|
524
547
|
return this.finish(args);
|
|
525
548
|
}
|
|
549
|
+
async admitNative(body) {
|
|
550
|
+
if (!this.store.admitNative)
|
|
551
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
552
|
+
return this.store.admitNative(body);
|
|
553
|
+
}
|
|
526
554
|
/** Read-only root selection, not a stop or a reservation. Generations come
|
|
527
555
|
* only from current observation or persisted unfinished relations/slots. A
|
|
528
556
|
* completed/retired contact never becomes a root just because its name matches.
|
|
@@ -743,7 +771,7 @@ export class ExecutionService {
|
|
|
743
771
|
throw new ExecutionError("EXECUTION_STOPPING");
|
|
744
772
|
if (!relation.source && record.source_session_id && state.stopping.some(value => value.session_id === record.source_session_id))
|
|
745
773
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
746
|
-
if (state
|
|
774
|
+
if (blocksSource(state, relation.source?.launch ?? null, record.source_session_id, slot))
|
|
747
775
|
throw new ExecutionError("EXECUTION_UNCONFIRMED");
|
|
748
776
|
if (relation.source) {
|
|
749
777
|
const source = this.runtime.observeSync(relation.source.launch.session_id);
|
package/dist/execution-store.js
CHANGED
|
@@ -81,6 +81,7 @@ export class FileExecutionStore {
|
|
|
81
81
|
directory;
|
|
82
82
|
lockTimeoutMs;
|
|
83
83
|
lockActor;
|
|
84
|
+
nativeAdmission = false;
|
|
84
85
|
constructor(directory, lockTimeoutMs = 2000, lockActor) {
|
|
85
86
|
this.directory = directory;
|
|
86
87
|
this.lockTimeoutMs = lockTimeoutMs;
|
|
@@ -151,7 +152,8 @@ export class FileExecutionStore {
|
|
|
151
152
|
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
152
153
|
let lock;
|
|
153
154
|
try {
|
|
154
|
-
|
|
155
|
+
if (!this.nativeAdmission)
|
|
156
|
+
lock = acquireExecutionLock(this.directory, this.lockActor);
|
|
155
157
|
}
|
|
156
158
|
catch (error) {
|
|
157
159
|
if (error instanceof ExecutionError)
|
|
@@ -169,10 +171,51 @@ export class FileExecutionStore {
|
|
|
169
171
|
return structuredClone(result);
|
|
170
172
|
}
|
|
171
173
|
finally {
|
|
174
|
+
if (lock)
|
|
175
|
+
releaseExecutionLock(lock);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async admitNative(body) {
|
|
179
|
+
// The marker exists only on a synchronous stack, never across an await.
|
|
180
|
+
// Re-entry shares the physical mutex but not a mutable state snapshot.
|
|
181
|
+
if (this.nativeAdmission)
|
|
182
|
+
return this.nativeBody(body);
|
|
183
|
+
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
184
|
+
const deadline = performance.now() + this.lockTimeoutMs;
|
|
185
|
+
let lock;
|
|
186
|
+
for (;;) {
|
|
187
|
+
try {
|
|
188
|
+
lock = acquireExecutionLock(this.directory, this.lockActor);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error.code !== "EEXIST" && !(error instanceof ExecutionError && error.code === "EXECUTION_BUSY"))
|
|
193
|
+
throw new ExecutionError("EXECUTION_STATE_UNAVAILABLE", "not_started");
|
|
194
|
+
if (performance.now() >= deadline)
|
|
195
|
+
throw new ExecutionError("EXECUTION_BUSY", "not_started");
|
|
196
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
this.nativeAdmission = true;
|
|
200
|
+
try {
|
|
201
|
+
return this.nativeBody(body);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.nativeAdmission = false;
|
|
172
205
|
releaseExecutionLock(lock);
|
|
173
206
|
}
|
|
174
207
|
}
|
|
208
|
+
nativeBody(body) {
|
|
209
|
+
const result = body();
|
|
210
|
+
if (result && typeof result.then === "function") {
|
|
211
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
212
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
175
216
|
async transaction(body, options = {}) {
|
|
217
|
+
if (this.nativeAdmission)
|
|
218
|
+
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
176
219
|
fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
|
|
177
220
|
let lock;
|
|
178
221
|
const deadline = performance.now() + this.lockTimeoutMs;
|
package/dist/execution-tools.js
CHANGED
|
@@ -126,15 +126,15 @@ async function runExecutionReceipt(service, meta, stage, body, targetSessionId,
|
|
|
126
126
|
return run();
|
|
127
127
|
}, stage === "launch" ? {
|
|
128
128
|
prepare: () => { expectedServer = service.prepareCreation(ref); return expectedServer; },
|
|
129
|
-
verifyServer: () => { if (
|
|
129
|
+
verifyServer: () => { if (expectedServer === undefined)
|
|
130
130
|
throw new ExecutionError("EXECUTION_UNSUPPORTED"); service.verifyCreationServer(ref, expectedServer); },
|
|
131
131
|
native: (session, nativeId, serverPid) => {
|
|
132
|
-
if (
|
|
132
|
+
if (expectedServer === undefined)
|
|
133
133
|
throw new ExecutionError("EXECUTION_UNSUPPORTED");
|
|
134
|
-
service.captureCreatedNative(ref, session, nativeId, expectedServer, serverPid);
|
|
134
|
+
return service.captureCreatedNative(ref, session, nativeId, expectedServer, serverPid);
|
|
135
135
|
},
|
|
136
136
|
launch: launch => service.bindCreatedLaunch(ref, launch),
|
|
137
|
-
} : undefined, prepare);
|
|
137
|
+
} : undefined, prepare, body => service.admitNative(body));
|
|
138
138
|
// A swallowed native refusal poisons the frame. Only a cleanly returned
|
|
139
139
|
// frame has a genuine successful action receipt to preserve on binding.
|
|
140
140
|
observed = action.value;
|
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,17 @@ 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
|
-
|
|
32
|
-
|
|
31
|
+
agentの貼付け・Enterの同期区間は、既存metadata mutexを非同期に取得してから実行する。
|
|
32
|
+
mutexをawait中へ持ち越さず、同じ同期stack内の各gateが発行記録を永続化してからnative作用へ進む。
|
|
33
|
+
検証済みsourceと同世代への通常sendがissuedである間も、そのsourceの送信先が別ならoutgoing RPCは継続できる。
|
|
34
|
+
incomingのslotを解放・書換・成功扱いせず、unknown・管理操作・世代不一致は従来どおり拒否する。
|
|
35
|
+
incomingの完了待ちをoutgoingへ課して相互送信を循環待ちにしない(ADR 0127)。
|
|
36
|
+
BellTeamのpromptなし明示管理launchは、管理close同様にserving process/request identityへ結合する。
|
|
37
|
+
promptやThroughline読出しを含む未登録送信、auto-name、remoteはこの管理例外へ含めない。
|
|
38
|
+
新launchは、既存POSIX serverのnative owner、またはserver未作成の完全な空観測を受付時と発行直前に確認する。
|
|
39
|
+
冷間時は最初のnew-session自身でserverを作り、その実receiptのnative IDとserver PIDを、作成後の安定したnative一覧とprocess開始識別子で照合する。
|
|
40
|
+
既存server時も冷間時も、作成したsessionのnative ID・server owner・core内部のlaunch世代を同じ要求slotへ段階結合する。
|
|
41
|
+
物理shell準備はnative IDへ固定し、未結合の同名世代を採用しない。冷間起動のための補助sessionや再送は作らない。
|
|
33
42
|
作成時のnative ID/server ownerをmanaged metadataへ保存し、private runtimeの世代読取と期待closeでも再照合する。
|
|
34
43
|
古いmetadataの欠測を現観測で補わず、既存席の自動登録は行わない。
|
|
35
44
|
証拠の無い新server起動、Windows固有のserver作成、auto-name/通常PTY/auth等はまだunsupported。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.61.
|
|
3
|
+
"version": "0.61.5",
|
|
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": [
|