aiterm-mcp 0.59.0 → 0.60.0

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.
@@ -11,7 +11,7 @@ import { spawnAgentControlCommand } from "../agent-resolver.js";
11
11
  import * as steer from "aiterm-steer-delivery";
12
12
  import { AITERM_PROFILE } from "../steer-profile.js";
13
13
  import { catalogInvalid, catalogUnavailable, checkedCatalog } from "../model-catalog.js";
14
- import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
14
+ import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, scanLinesNewestFirst, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
15
15
  export function realCodexHome() {
16
16
  return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
17
17
  }
@@ -225,6 +225,18 @@ function codexLaunchVerdictMemoFor(launchId) {
225
225
  }
226
226
  return memo;
227
227
  }
228
+ // 起動ごとの、「この起動の会話ではない」と決まったrollout。古いrollout、Codex CLIのroot会話でない物、最初のturnまでにこの起動の印が無い物。
229
+ const codexLaunchOthers = new Map();
230
+ function codexLaunchOthersFor(launchId) {
231
+ let others = codexLaunchOthers.get(launchId);
232
+ if (!others) {
233
+ if (codexLaunchOthers.size >= CODEX_LAUNCH_VERDICT_MEMO_MAX)
234
+ codexLaunchOthers.delete(codexLaunchOthers.keys().next().value);
235
+ others = new Set();
236
+ codexLaunchOthers.set(launchId, others);
237
+ }
238
+ return others;
239
+ }
228
240
  /** 結び付けた会話(vendor_session_id)のroot rollout。会話の切り替えは追わない。完了の読み位置(cursor)はこのfileの位置。 */
229
241
  export function codexBoundTranscript(meta) {
230
242
  if (meta.kind !== "codex" || !meta.codex_home || !meta.vendor_session_id)
@@ -297,7 +309,17 @@ export function codexRootTranscript(meta) {
297
309
  return bound ? codexSwitchedTranscript(meta, bound) ?? bound : null;
298
310
  }
299
311
  if (meta.hook_route === "shared_codex_home") {
300
- const matches = listCodexTranscripts(meta.codex_home).filter((file) => codexTranscriptMatchesLaunch(file, meta));
312
+ // まだ会話を結び付けていない席(起こしてから、2つめの送信までの間)。この起動の印を持つrolloutを探す。
313
+ // 「この起動の物でない」と決まったrolloutは、後から変わらないので覚えて読み直さない(席の今の番は、結び付く前の席も見回りで聞かれる。ADR 0122)。
314
+ const others = codexLaunchOthersFor(meta.launch_id);
315
+ const matches = listCodexTranscripts(meta.codex_home).filter((file) => {
316
+ if (others.has(file))
317
+ return false;
318
+ const { verdict } = codexLaunchVerdict(file, meta);
319
+ if (verdict === "other")
320
+ others.add(file);
321
+ return verdict === "match";
322
+ });
301
323
  if (matches.length > 1) {
302
324
  throw new AitermError("共有CODEX_HOMEに同じlaunch markerのroot rolloutが複数あります。sessionを閉じて起動し直してください。", 2);
303
325
  }
@@ -306,7 +328,7 @@ export function codexRootTranscript(meta) {
306
328
  return listCodexTranscripts(meta.codex_home)[0] ?? null;
307
329
  }
308
330
  /** rolloutの会話ID。結び付けた会話のrolloutならそのID、会話が切り替わった後のrolloutなら、その頭に書かれたID。 */
309
- function codexRolloutSessionId(meta, transcript) {
331
+ export function codexRolloutSessionId(meta, transcript) {
310
332
  if (meta.vendor_session_id && path.basename(transcript).includes(meta.vendor_session_id))
311
333
  return meta.vendor_session_id;
312
334
  return codexTranscriptSessionId(transcript) ?? codexRolloutCreation(transcript)?.id ?? meta.vendor_session_id;
@@ -383,12 +405,33 @@ export function codexUsageLimit(record) {
383
405
  }
384
406
  // 完了待ちが、会話の切り替えを確かめる間隔。送った文はすぐ記録へ書かれるので、普通のturnはここへ来ない。
385
407
  const CODEX_SWITCH_CHECK_MS = 3_000;
408
+ // 完了待ちが、記録を1回に読む大きさ。
409
+ const CODEX_TRANSCRIPT_READ_CHUNK_BYTES = 4 * 1024 * 1024;
410
+ /** 1行がturnの境界ならその境界、境界でなければundefined、境界の語を含むのに読めない行ならnull。 */
411
+ function codexTurnBoundaryLine(line) {
412
+ if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
413
+ return undefined;
414
+ let record;
415
+ try {
416
+ record = JSON.parse(line);
417
+ }
418
+ catch {
419
+ return null;
420
+ }
421
+ if (record?.type !== "event_msg")
422
+ return undefined;
423
+ const type = record?.payload?.type;
424
+ return type === "task_started" || type === "task_complete" || type === "turn_aborted"
425
+ ? { type, turn_id: record.payload.turn_id, reason: record.payload.reason } : undefined;
426
+ }
386
427
  /**
387
428
  * この起動のroot rolloutの、末尾から見て最初のturnの境界。
388
429
  * Codexはturnの開始でtask_started、終わりでtask_complete、止めた時はturn_aborted(reasonはinterrupted)を書く(0.160.1の実記録)。
389
430
  * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull。
431
+ * 読む範囲は、`reach`が"tail"なら末尾の1MiB(終わった番の境界は末尾の近くにある。見回りで何度も呼ぶ所はこちら)、
432
+ * "whole"なら境界に当たるまで先頭へさかのぼる(動いている番の開始は、番が長いと末尾の1MiBより前にある)。
390
433
  */
391
- function codexLatestTurnBoundary(meta) {
434
+ function codexLatestTurnBoundary(meta, reach) {
392
435
  let transcript;
393
436
  try {
394
437
  transcript = codexRootTranscript(meta);
@@ -398,24 +441,15 @@ function codexLatestTurnBoundary(meta) {
398
441
  }
399
442
  if (!transcript)
400
443
  return null;
444
+ if (reach === "whole")
445
+ return scanLinesNewestFirst(transcript, codexTurnBoundaryLine, AGENT_TURN_BOUNDARY_TAIL_BYTES) ?? null;
401
446
  const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
402
447
  if (!lines)
403
448
  return null;
404
449
  for (const line of lines) {
405
- if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
406
- continue;
407
- let record;
408
- try {
409
- record = JSON.parse(line);
410
- }
411
- catch {
412
- return null;
413
- }
414
- if (record?.type !== "event_msg")
415
- continue;
416
- const type = record?.payload?.type;
417
- if (type === "task_started" || type === "task_complete" || type === "turn_aborted")
418
- return { type, turn_id: record.payload.turn_id, reason: record.payload.reason };
450
+ const boundary = codexTurnBoundaryLine(line);
451
+ if (boundary !== undefined)
452
+ return boundary;
419
453
  }
420
454
  return null;
421
455
  }
@@ -425,7 +459,7 @@ function codexLatestTurnBoundary(meta) {
425
459
  * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
426
460
  */
427
461
  export function codexTurnSettled(meta) {
428
- const boundary = codexLatestTurnBoundary(meta);
462
+ const boundary = codexLatestTurnBoundary(meta, "tail");
429
463
  if (boundary?.type === "task_complete")
430
464
  return typeof boundary.turn_id === "string" && boundary.turn_id !== "";
431
465
  // 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
@@ -433,11 +467,13 @@ export function codexTurnSettled(meta) {
433
467
  }
434
468
  /**
435
469
  * この起動のroot rolloutの上で動いているturnのID。末尾から見て最初の境界がtask_startedの時だけ返す。
436
- * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull(分からない時は「動いている」と数えない)。
470
+ * rolloutが無い、この起動に結び付かない、書いている途中、境界が1つも無い時はnull(分からない時は「動いている」と数えない)。
471
+ * 開始の印は、境界に当たるまで記録をさかのぼって探す。末尾の1MiBだけを読むと、長い番(開始から1MiBより多く書いた番)を
472
+ * 動いていると読めず、回答を流している間に届いた文が、新しいturnの道で入力待ちを30秒待って断られる(実被弾 2026-10-13)。
437
473
  * Codexは、processが生きている間、turnの終わりを必ず書く。書かずに終わるのは落ちた時なので、呼ぶ側はprocessの生死も見る。
438
474
  */
439
475
  export function codexRunningTurn(meta) {
440
- const boundary = codexLatestTurnBoundary(meta);
476
+ const boundary = codexLatestTurnBoundary(meta, "whole");
441
477
  return boundary?.type === "task_started" && typeof boundary.turn_id === "string" && boundary.turn_id !== "" ? boundary.turn_id : null;
442
478
  }
443
479
  export function latestCodexCompletion(meta, readTranscriptLines) {
@@ -465,7 +501,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
465
501
  let transcript = anchored();
466
502
  let startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
467
503
  let cursor = startOffset;
468
- let carry = "";
504
+ let carry = Buffer.alloc(0);
469
505
  let malformedEvents = 0;
470
506
  let discardLeadingFragment = false;
471
507
  let initializedBoundary = false;
@@ -494,6 +530,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
494
530
  signal?.throwIfAborted();
495
531
  if (!fs.existsSync(metadataFile))
496
532
  return observation("closed");
533
+ let behind = false;
497
534
  transcript ??= anchored();
498
535
  if (transcript && meta.vendor_session_id && cursor === startOffset && performance.now() >= switchCheckAt) {
499
536
  switchCheckAt = performance.now() + CODEX_SWITCH_CHECK_MS;
@@ -520,7 +557,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
520
557
  }
521
558
  transcript = current;
522
559
  startOffset = cursor;
523
- carry = "";
560
+ carry = Buffer.alloc(0);
524
561
  initializedBoundary = false;
525
562
  discardLeadingFragment = false;
526
563
  }
@@ -537,18 +574,26 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
537
574
  if (size < cursor) {
538
575
  throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
539
576
  }
540
- if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
541
- throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
542
- }
543
577
  if (size > cursor) {
544
- carry += readFileRange(transcript, cursor, size).toString("utf8");
545
- cursor = size;
546
- const parts = carry.split("\n");
547
- carry = parts.pop() ?? "";
578
+ // 1回に読むのは決めた大きさまで。番の大きさには上限を置かない。前は、読み始めから16MiBを超えて書いた番で、待ちが誤りで止まった。
579
+ // 席の今の番(ADR 0122)から待つ呼び手は、長く続いた番を頭から読む。
580
+ const chunk = readFileRange(transcript, cursor, Math.min(size, cursor + CODEX_TRANSCRIPT_READ_CHUNK_BYTES));
581
+ cursor += chunk.length;
582
+ behind = chunk.length > 0 && cursor < size;
583
+ const joined = carry.length ? Buffer.concat([carry, chunk]) : chunk;
584
+ const last = joined.lastIndexOf(0x0a);
585
+ const parts = last < 0 ? [] : joined.subarray(0, last).toString("utf8").split("\n");
586
+ carry = last < 0 ? joined : joined.subarray(last + 1);
548
587
  if (discardLeadingFragment && parts.length > 0) {
549
588
  parts.shift();
550
589
  discardLeadingFragment = false;
551
590
  }
591
+ // 改行が来ないまま上限を超えた行は、番の境界の行ではない。持たずに、その行の終わりまで読み飛ばす。
592
+ if (carry.length > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
593
+ carry = Buffer.alloc(0);
594
+ discardLeadingFragment = true;
595
+ malformedEvents++;
596
+ }
552
597
  const harnessSessionId = meta.vendor_session_id ? codexRolloutSessionId(meta, transcript) : codexTranscriptSessionId(transcript);
553
598
  for (const line of parts) {
554
599
  if (!line.trim())
@@ -583,6 +628,9 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
583
628
  }
584
629
  }
585
630
  }
631
+ // まだ読んでいない分が在る間は、待たずに読み進める(一度だけ見る照会も、今在る分は全部読んでから答える)。
632
+ if (behind)
633
+ continue;
586
634
  if (performance.now() >= deadline)
587
635
  return observation(timeout === 0 ? "running" : "timeout");
588
636
  await sleep(AGENT_DONE_POLL_MS);
package/dist/index.js CHANGED
@@ -25,7 +25,8 @@ import { claudeParentFromRequest, discardClaudeHookRequest } from "./claude-pare
25
25
  import { parentDeliveryDiagnostic } from "./parent-hook-diagnostic.js";
26
26
  import { cursorParentFromRequest, isCursorMcpClient } from "./cursor-parent-receiver.js";
27
27
  import { waitProcessCommandLine } from "./cursor-parent-receive.js";
28
- import { INTERIM_RESULT_META_KEY, interimRequestFromMeta } from "./interim-words.js";
28
+ import { INTERIM_RESULT_META_KEY, interimCallerFromMeta, interimRequestFromMeta } from "./interim-words.js";
29
+ import { SEAT_TURNS_RESULT_META_KEY, STEER_RESULT_META_KEY } from "./steer-follow.js";
29
30
  // package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
30
31
  // createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
31
32
  // engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
@@ -368,8 +369,11 @@ registerRemoteAwareTool("pty_send", {
368
369
  if (rtk)
369
370
  throw new Error("agent session への dispatch は rtk:true と併用できません");
370
371
  delivery = await deliveryForRequest(extra);
371
- const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send, preface });
372
+ // BellTeamのため(ADR 0122): 差し込みになった時、その文を取った番の終わりを待つ手がかりを返す。印の無い呼び手には何も足さない。
373
+ const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send, preface,
374
+ follow: interimCallerFromMeta(extra._meta) });
372
375
  if (receipt.schema === "aiterm.agent-steer.v1") {
376
+ const follow = receipt.follow ?? null;
373
377
  return {
374
378
  content: [
375
379
  {
@@ -382,14 +386,15 @@ registerRemoteAwareTool("pty_send", {
382
386
  schema: "aiterm.pty-send-result.v1",
383
387
  mode: "agent_steer",
384
388
  session_id: receipt.session_id,
385
- event_cursor: null,
386
- wait_process: null,
389
+ event_cursor: follow ? follow.event_cursor : null,
390
+ wait_process: follow ? core.agentWaitProcess(receipt.session_id, follow.event_cursor, {}, follow.steer_id) : null,
387
391
  launch_id: receipt.launch_id,
388
392
  vendor: receipt.vendor,
389
393
  harness: receipt.harness,
390
394
  submit_residue: null,
391
395
  pane_input_recovery: receipt.pane_input_recovery,
392
396
  },
397
+ ...(follow ? { _meta: { [STEER_RESULT_META_KEY]: { schema: "aiterm.steer-ref.v1", steer_id: follow.steer_id, interim_from: follow.interim_from } } } : {}),
393
398
  };
394
399
  }
395
400
  const waited = completionWait(delivery, receipt.session_id, receipt.event_cursor);
@@ -624,15 +629,27 @@ registerRemoteAwareTool("pty_list", {
624
629
  environment: z.record(z.string(), z.string().nullable()),
625
630
  })),
626
631
  },
627
- }, async ({ env_keys }) => {
632
+ }, async ({ env_keys }, extra) => {
628
633
  try {
629
634
  const result = core.listSessionsResult(env_keys);
630
- return { ...ok(core.listSessions(result)), structuredContent: { ...result } };
635
+ return { ...ok(core.listSessions(result)), structuredContent: { ...result }, ...seatTurnsMeta(result.sessions, extra._meta) };
631
636
  }
632
637
  catch (e) {
633
638
  return fail(e);
634
639
  }
635
640
  });
641
+ // BellTeamのため(ADR 0122): 要求の_metaに aiterm/caller="BellTeam" がある時だけ、結果の_metaへ席の今の番を載せる。
642
+ // 説明と引数には出さない(steer-follow.ts の冒頭)。それ以外の呼び手には何も足さない。
643
+ function seatTurnsMeta(sessions, requestMeta) {
644
+ if (!interimCallerFromMeta(requestMeta))
645
+ return {};
646
+ try {
647
+ return { _meta: { [SEAT_TURNS_RESULT_META_KEY]: core.readSeatTurns(sessions.filter(session => session.harness !== null).map(session => session.session_id)) } };
648
+ }
649
+ catch (e) {
650
+ return { _meta: { [SEAT_TURNS_RESULT_META_KEY]: { schema: "aiterm.seat-turns.v1", error: e instanceof Error ? e.message : String(e) } } };
651
+ }
652
+ }
636
653
  const nativeProcessIdentitySchema = z.object({
637
654
  pid: z.number().int(), process_group_id: z.number().int().nullable(),
638
655
  started_identity: z.string(), argv_digest: z.string(),
@@ -675,7 +692,10 @@ function interimWordsMeta(sessionId, requestMeta) {
675
692
  if (!request)
676
693
  return {};
677
694
  try {
678
- const words = core.readSessionInterimWords(sessionId, request.after);
695
+ // 読み始める位置が付いているのに読めない時は、最後のdispatchの言葉を返さない(呼び手が別の番の言葉を出してしまう)。
696
+ if (request.from_invalid)
697
+ throw new Error("INTERIM_FROM_INVALID: aiterm/interim_from の値を読めません");
698
+ const words = core.readSessionInterimWords(sessionId, request.after, request.from ?? null);
679
699
  return words ? { _meta: { [INTERIM_RESULT_META_KEY]: words } } : {};
680
700
  }
681
701
  catch (e) {
@@ -24,21 +24,47 @@ export const INTERIM_CALLER = "BellTeam";
24
24
  export const INTERIM_AFTER_META_KEY = "aiterm/interim_after";
25
25
  /** 結果の_metaで返すキー。 */
26
26
  export const INTERIM_RESULT_META_KEY = "aiterm/interim_words";
27
+ /**
28
+ * 読み始める位置。差し込みの返りと「席の今の番」が渡す文字列(中身は呼び手に読ませない)。
29
+ * 指定した時は、最後のdispatchの境界ではなく、その位置より後の言葉を返す(ADR 0122)。
30
+ */
31
+ export const INTERIM_FROM_META_KEY = "aiterm/interim_from";
32
+ export function interimFromToken(from) {
33
+ return `v1:${from.event_cursor}:${from.unit === "byte" ? "b" : "u"}:${from.boundary}`;
34
+ }
35
+ export function parseInterimFrom(token) {
36
+ if (typeof token !== "string")
37
+ return null;
38
+ const match = /^v1:(\d{1,15}):([bu]):(\d{1,15})$/.exec(token);
39
+ if (!match)
40
+ return null;
41
+ return { event_cursor: Number(match[1]), unit: match[2] === "b" ? "byte" : "user", boundary: Number(match[3]) };
42
+ }
27
43
  export function agentInterimBoundaryPath(name, launchId) {
28
44
  assertSessionName(name);
29
45
  if (!LAUNCH_ID_RE.test(launchId))
30
46
  throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
31
47
  return path.join(agentsDir(), `${name}.${launchId}.interim.json`);
32
48
  }
33
- /** 要求の_metaがBellTeamの印を持つ時だけ、前回のseqを返す。それ以外はnull(何も足さない)。 */
49
+ /** 要求の_metaがBellTeamの印を持つか。 */
50
+ export function interimCallerFromMeta(meta) {
51
+ return !!meta && typeof meta === "object" && meta[INTERIM_CALLER_META_KEY] === INTERIM_CALLER;
52
+ }
53
+ /**
54
+ * 要求の_metaがBellTeamの印を持つ時だけ、前回のseqを返す。それ以外はnull(何も足さない)。
55
+ * 読み始める位置(aiterm/interim_from)が付いていれば、fromに入れる。付いているのに読めない時はfromがnullでfrom_invalidがtrue
56
+ * (最後のdispatchの言葉を黙って返すと、呼び手は別の番の言葉を出してしまう)。
57
+ */
34
58
  export function interimRequestFromMeta(meta) {
35
- if (!meta || typeof meta !== "object")
59
+ if (!interimCallerFromMeta(meta))
36
60
  return null;
37
61
  const record = meta;
38
- if (record[INTERIM_CALLER_META_KEY] !== INTERIM_CALLER)
39
- return null;
40
- const after = record[INTERIM_AFTER_META_KEY];
41
- return { after: typeof after === "number" && Number.isSafeInteger(after) && after > 0 ? after : 0 };
62
+ const raw = record[INTERIM_AFTER_META_KEY];
63
+ const after = typeof raw === "number" && Number.isSafeInteger(raw) && raw > 0 ? raw : 0;
64
+ if (record[INTERIM_FROM_META_KEY] === undefined)
65
+ return { after };
66
+ const from = parseInterimFrom(record[INTERIM_FROM_META_KEY]);
67
+ return from ? { after, from } : { after, from: null, from_invalid: true };
42
68
  }
43
69
  function parse(line) {
44
70
  if (!line.trim())
@@ -238,8 +264,54 @@ function readInterimBoundary(meta) {
238
264
  return null;
239
265
  return value;
240
266
  }
241
- /** 現在(または直前)のturnで書かれた合間の言葉のうち、seqがafterより後のもの。 */
242
- export function readInterimWords(meta, after) {
267
+ /**
268
+ * 今の時点の読み始めの位置。差し込みを打つ直前に取る(その差し込みより後に書かれた言葉だけを読むため)。
269
+ * Claude・Codex・Cursorは記録のbyte位置、Grokは記録中のuser発話の数(「今すぐ送る」で入った文は、次のuser発話になる)。
270
+ * Cursorは、番が動いている間は記録を足すだけなので、byte位置が動かない(末尾の行を書き換えるのは、次の番の開始の時)。
271
+ */
272
+ export function interimFromNow(meta, eventCursor) {
273
+ if (meta.kind === "grok")
274
+ return { event_cursor: eventCursor, unit: "user", boundary: grokUserCount(readLines(grokChatHistory(meta))) };
275
+ let file = null;
276
+ try {
277
+ file = meta.kind === "claude" ? claudeSessionTranscriptPath(meta)
278
+ : meta.kind === "codex" ? (meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta))
279
+ : cursorTranscript(meta);
280
+ }
281
+ catch { /* 記録を決められない時は、頭から */ }
282
+ return { event_cursor: eventCursor, unit: "byte", boundary: file ? safeStatSize(file) : 0 };
283
+ }
284
+ /** 渡された位置より後に書かれた合間の言葉。位置の単位がharnessと合わない時は、言葉なし。 */
285
+ function interimWordsFrom(meta, from) {
286
+ if (meta.kind === "grok")
287
+ return from.unit === "user" ? grokInterimWords(readLines(grokChatHistory(meta)), from.boundary) : [];
288
+ if (from.unit !== "byte")
289
+ return [];
290
+ if (meta.kind === "claude")
291
+ return claudeInterimWords(readLines(claudeSessionTranscriptPath(meta), from.boundary));
292
+ if (meta.kind === "codex")
293
+ return codexInterimWords(readLines(meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta), from.boundary));
294
+ // 位置より後の行だけを渡すので、user発話の数では絞らない。
295
+ return cursorInterimWords(readLines(cursorTranscript(meta), from.boundary), -1);
296
+ }
297
+ /**
298
+ * 現在(または直前)のturnで書かれた合間の言葉のうち、seqがafterより後のもの。
299
+ * fromを渡した時は、最後のdispatchの境界ではなく、その位置より後の言葉(seqはその位置から1で数える)。
300
+ */
301
+ export function readInterimWords(meta, after, from = null) {
302
+ if (from) {
303
+ const words = interimWordsFrom(meta, from).map((word, index) => ({ seq: index + 1, ...word }));
304
+ return {
305
+ schema: "aiterm.interim-words.v1",
306
+ session_id: meta.aiterm_session,
307
+ launch_id: meta.launch_id,
308
+ harness: agentHarness(meta.kind),
309
+ event_cursor: from.event_cursor,
310
+ operation_id: null,
311
+ last_seq: words.length,
312
+ words: words.filter((word) => word.seq > after),
313
+ };
314
+ }
243
315
  const recorded = readInterimBoundary(meta);
244
316
  // dispatchの記録が無ければ、起動時promptのturnだけを記録の先頭から読む。promptなしで起動してまだ送っていない時は無い。
245
317
  const launchTurn = !recorded && meta.initial_prompt !== "none";
@@ -307,7 +307,8 @@ export class ParentDeliveryManager {
307
307
  job.capture = (async () => {
308
308
  if (completion.launch_id !== job.record.boundary.launch_id)
309
309
  throw new AitermError("PARENT_DELIVERY_LAUNCH_CHANGED: 子のlaunchが置換されました", 2);
310
- if (completion.outcome === "running" || completion.outcome === "timeout")
310
+ // not_takenは差し込みを追う待ちだけが返す。親配送の待ちは差し込みを追わないので、来ない。
311
+ if (completion.outcome === "running" || completion.outcome === "timeout" || completion.outcome === "not_taken")
311
312
  throw new AitermError("PARENT_DELIVERY_NOT_COMPLETED: 子の完了を確認できません", 2);
312
313
  const record = job.record;
313
314
  record.child_outcome = completion.outcome;