@astrosheep/keiyaku 1.0.0 → 1.0.2

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.
Files changed (123) hide show
  1. package/README.md +73 -56
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms_v2.js +103 -0
  4. package/build/agents/harness/activity-values.js +54 -0
  5. package/build/agents/harness/event-channel.js +55 -20
  6. package/build/agents/harness/events.js +47 -1
  7. package/build/agents/harness/execution-handle.js +292 -61
  8. package/build/agents/harness/index.js +103 -121
  9. package/build/agents/harness/pump.js +36 -47
  10. package/build/agents/harness/runtime.js +3 -0
  11. package/build/agents/launch-snapshot_v2.js +348 -0
  12. package/build/agents/opencode-sdk.js +42 -169
  13. package/build/agents/providers/claude-agent-sdk.js +240 -79
  14. package/build/agents/providers/codex-app-server.js +280 -72
  15. package/build/agents/providers/codex-sdk.js +159 -41
  16. package/build/agents/providers/event-type-tail.js +16 -0
  17. package/build/agents/providers/opencode-sdk.js +165 -59
  18. package/build/agents/providers/pi.js +254 -42
  19. package/build/agents/selector_v2.js +122 -0
  20. package/build/cli/commands/akuma.js +120 -42
  21. package/build/cli/completion.js +6 -7
  22. package/build/cli/flags.js +36 -10
  23. package/build/cli/help.js +42 -26
  24. package/build/cli/index.js +225 -93
  25. package/build/cli/parse.js +194 -75
  26. package/build/cli/projection-address.js +11 -0
  27. package/build/cli/render/address.js +88 -0
  28. package/build/cli/render/arc.js +10 -10
  29. package/build/cli/render/call.js +59 -0
  30. package/build/cli/render/compact-text.js +3 -0
  31. package/build/cli/render/errors.js +4 -2
  32. package/build/cli/render/format.js +6 -0
  33. package/build/cli/render/line-width.js +70 -0
  34. package/build/cli/render/misc.js +7 -4
  35. package/build/cli/render/petition.js +4 -3
  36. package/build/cli/render/projection-activity.js +201 -0
  37. package/build/cli/render/shared.js +46 -12
  38. package/build/cli/render/status.js +59 -10
  39. package/build/cli/render/tool-ledger-rollup.js +74 -0
  40. package/build/cli/render/tool-presentation.js +97 -0
  41. package/build/cli/render/wait.js +229 -0
  42. package/build/cli/skills-install.js +42 -24
  43. package/build/cli/subagent-guard.js +1 -1
  44. package/build/config/akuma-loader_v2.js +222 -0
  45. package/build/config/env-keys.js +0 -4
  46. package/build/config/env.js +0 -6
  47. package/build/config/settings.js +325 -12
  48. package/build/core/addressing.js +224 -0
  49. package/build/core/amend.js +30 -47
  50. package/build/core/arc.js +68 -60
  51. package/build/core/bind.js +105 -25
  52. package/build/core/call-persist.js +69 -0
  53. package/build/core/call.js +658 -0
  54. package/build/core/claim.js +72 -44
  55. package/build/core/command-io.js +19 -19
  56. package/build/core/context.js +11 -36
  57. package/build/core/draft.js +2 -3
  58. package/build/core/entry.js +101 -6
  59. package/build/core/execution-coordinate.js +62 -0
  60. package/build/core/execution-pact.js +141 -0
  61. package/build/core/forfeit.js +2 -3
  62. package/build/core/hints.js +10 -10
  63. package/build/core/ids.js +6 -0
  64. package/build/core/ledger.js +16 -1
  65. package/build/core/petition-claim-gates.js +14 -12
  66. package/build/core/petition-claim.js +1 -0
  67. package/build/core/petition-run.js +6 -2
  68. package/build/core/petition.js +63 -21
  69. package/build/core/places.js +106 -13
  70. package/build/core/projection/generation.js +412 -0
  71. package/build/core/projection/heart.js +316 -0
  72. package/build/core/projection/identity.js +81 -0
  73. package/build/core/projection/leash.js +87 -0
  74. package/build/core/projection/mint.js +154 -0
  75. package/build/core/projection-activity.js +219 -0
  76. package/build/core/projection-coordinate.js +35 -0
  77. package/build/core/projection-core.js +69 -463
  78. package/build/core/projection-generation-continuation.js +31 -0
  79. package/build/core/projection-generation-execution.js +148 -0
  80. package/build/core/projection-generation-identity.js +17 -0
  81. package/build/core/projection-generation-launcher.js +90 -0
  82. package/build/core/projection-generation-process.js +43 -0
  83. package/build/core/projection-generation-runner.js +144 -0
  84. package/build/core/projection-generation-runtime.js +15 -0
  85. package/build/core/projection-generation-store.js +707 -0
  86. package/build/core/projection-identity.js +11 -0
  87. package/build/core/projection-life-observer.js +87 -0
  88. package/build/core/projection-life-protocol.js +95 -0
  89. package/build/core/projection-mint.js +96 -24
  90. package/build/core/projection-runner-lock.js +230 -0
  91. package/build/core/projection-status.js +207 -107
  92. package/build/core/projection-tells.js +401 -0
  93. package/build/core/projection-wait.js +155 -0
  94. package/build/core/projection-wake.js +109 -231
  95. package/build/core/queue_v2.js +342 -0
  96. package/build/core/registry.js +6 -3
  97. package/build/core/render.js +63 -1
  98. package/build/core/renew.js +80 -53
  99. package/build/core/scope.js +186 -137
  100. package/build/core/seal.js +16 -19
  101. package/build/core/status.js +88 -25
  102. package/build/core/stored-agent-event.js +101 -0
  103. package/build/core/transcripts.js +214 -228
  104. package/build/core/verdict.js +27 -15
  105. package/build/core/worktree-bootstrap.js +187 -0
  106. package/build/core/worktree-path.js +72 -18
  107. package/build/flow-error.js +4 -6
  108. package/build/generated/version.js +1 -1
  109. package/build/git/branches.js +6 -1
  110. package/build/index.js +8 -5
  111. package/build/keiyaku.js +1 -3
  112. package/package.json +6 -5
  113. package/skills/keiyaku/SKILL.md +6 -47
  114. package/skills/keiyaku-akuma/SKILL.md +75 -0
  115. package/build/agents/effective-policy.js +0 -32
  116. package/build/agents/index.js +0 -114
  117. package/build/agents/selector.js +0 -28
  118. package/build/cli/render/summon.js +0 -17
  119. package/build/cli/render/tell.js +0 -11
  120. package/build/config/akuma-loader.js +0 -268
  121. package/build/core/queue.js +0 -73
  122. package/build/core/summon-persist.js +0 -73
  123. package/build/core/summon.js +0 -377
@@ -3,22 +3,111 @@ import { z } from "zod";
3
3
  import { appendDebugLog } from "../../telemetry/debug-log.js";
4
4
  import { SUBAGENT_SESSION_VERSION } from "../session.js";
5
5
  import { createProjectionDriver } from "../harness/execution-handle.js";
6
+ import { boundActivityExcerpt, diffstatFromUnifiedPatch } from "../harness/activity-values.js";
7
+ import { AGENT_PLAN_ITEM_TEXT_MAX_CHARS, AGENT_PLAN_MAX_ITEMS, AGENT_THOUGHT_TEXT_MAX_CHARS, } from "../harness/events.js";
6
8
  import { buildSubagentEnv, createAbortError, emitProviderEvidence, sha256Utf8 } from "../harness/runtime.js";
7
- import { coerceString } from "../../agents/index.js";
9
+ import { coerceString } from "../harness/runtime.js";
10
+ /** Identity-only opacity boundary for codex-app-server turn/start effort. */
11
+ function toCodexAppServerEffort(effort) {
12
+ return effort;
13
+ }
14
+ const CODEX_APP_SERVER_NOTIFICATION_ACKNOWLEDGED = new Set(["item/agentMessage/delta"]);
15
+ const CODEX_APP_SERVER_NOTIFICATION_MAPPED = new Set([
16
+ "item/completed",
17
+ "turn/started",
18
+ "turn/completed",
19
+ "error",
20
+ ]);
21
+ function codexAppServerNotificationDisposition(method) {
22
+ if (CODEX_APP_SERVER_NOTIFICATION_ACKNOWLEDGED.has(method))
23
+ return "acknowledged-unpersisted";
24
+ if (CODEX_APP_SERVER_NOTIFICATION_MAPPED.has(method))
25
+ return "mapped";
26
+ return "unknown";
27
+ }
8
28
  /** Session handle for codex-app-server: thread id plus optional CODEX_HOME for cross-process resume. */
9
29
  export const codexAppServerSessionSchema = z.object({
10
30
  threadId: z.string().trim().min(1),
11
31
  codexHome: z.string().trim().min(1).optional(),
12
32
  });
33
+ function appServerRecord(value) {
34
+ return value !== null && typeof value === "object" && !Array.isArray(value)
35
+ ? value
36
+ : undefined;
37
+ }
38
+ const CODEX_APP_SERVER_ACTIVITY_ITEM_TYPES = [
39
+ "commandExecution",
40
+ "fileChange",
41
+ "webSearch",
42
+ "mcpToolCall",
43
+ ];
44
+ function isCodexAppServerActivityItemType(value) {
45
+ return CODEX_APP_SERVER_ACTIVITY_ITEM_TYPES.includes(value);
46
+ }
47
+ function appServerActivityCall(itemType, item) {
48
+ switch (itemType) {
49
+ case "commandExecution":
50
+ return typeof item.command === "string"
51
+ ? {
52
+ kind: "run",
53
+ command: item.command,
54
+ ...(typeof item.description === "string" && item.description ? { description: item.description } : {}),
55
+ }
56
+ : { kind: "other", display: itemType };
57
+ case "fileChange": {
58
+ const changes = Array.isArray(item.changes)
59
+ ? item.changes.flatMap((entry) => {
60
+ const change = appServerRecord(entry);
61
+ if (!change || typeof change.path !== "string")
62
+ return [];
63
+ if (change.kind !== "add" && change.kind !== "update" && change.kind !== "delete")
64
+ return [];
65
+ const op = change.kind;
66
+ return [{ op, path: change.path }];
67
+ })
68
+ : [];
69
+ if (changes.length > 0)
70
+ return { kind: "fileChange", changes };
71
+ return { kind: "other", display: typeof item.path === "string" ? `fileChange ${item.path}` : itemType };
72
+ }
73
+ case "webSearch":
74
+ return typeof item.query === "string" ? { kind: "search", query: item.query } : { kind: "other", display: itemType };
75
+ case "mcpToolCall":
76
+ return typeof item.server === "string" && typeof item.tool === "string"
77
+ ? { kind: "other", display: `${item.server}.${item.tool}` }
78
+ : { kind: "other", display: itemType };
79
+ }
80
+ }
81
+ function appServerActivityResult(item) {
82
+ const exitCode = typeof item.exitCode === "number" && Number.isFinite(item.exitCode) ? item.exitCode : undefined;
83
+ const message = typeof item.aggregatedOutput === "string" && item.aggregatedOutput
84
+ ? boundActivityExcerpt(item.aggregatedOutput)
85
+ : typeof item.output === "string" && item.output
86
+ ? boundActivityExcerpt(item.output)
87
+ : typeof item.error === "string" && item.error
88
+ ? boundActivityExcerpt(item.error)
89
+ : undefined;
90
+ const details = item.details !== null && typeof item.details === "object" && !Array.isArray(item.details)
91
+ ? item.details
92
+ : undefined;
93
+ const patch = typeof details?.patch === "string" ? details.patch : undefined;
94
+ const diffstat = patch ? diffstatFromUnifiedPatch(patch) : undefined;
95
+ return {
96
+ status: item.status === "failed" || (exitCode !== undefined && exitCode !== 0) ? "error" : "ok",
97
+ ...(message ? { message } : {}),
98
+ ...(exitCode === undefined ? {} : { exitCode }),
99
+ ...(diffstat ? { diffstat } : {}),
100
+ };
101
+ }
13
102
  const CODEX_APP_SERVER_PROVIDER = "codex-app-server";
14
103
  const MINIMUM_CODEX_APP_SERVER_VERSION = { major: 0, minor: 143, patch: 0 };
15
104
  /** Non-interactive defaults for turn/start (faye #316 policy A). */
16
105
  export const CODEX_APP_SERVER_TURN_APPROVAL_POLICY = "never";
17
- export function buildCodexAppServerDefaultSandboxPolicy(cwd) {
106
+ export function buildCodexAppServerWorkspaceWriteSandboxPolicy(input) {
18
107
  return {
19
108
  type: "workspaceWrite",
20
- writableRoots: [cwd],
21
- networkAccess: false,
109
+ writableRoots: [...input.writableRoots],
110
+ networkAccess: input.networkAccess,
22
111
  excludeTmpdirEnvVar: false,
23
112
  excludeSlashTmp: false,
24
113
  };
@@ -43,6 +132,40 @@ export function configOwnsSandboxPolicy(config) {
43
132
  }
44
133
  return false;
45
134
  }
135
+ function extractReasoningSummary(item) {
136
+ const summary = item.summary;
137
+ const text = typeof summary === "string"
138
+ ? summary
139
+ : Array.isArray(summary)
140
+ ? summary.map((part) => {
141
+ if (typeof part === "string")
142
+ return part;
143
+ if (!part || typeof part !== "object")
144
+ return "";
145
+ const record = part;
146
+ if (record.type !== "summary_text" && record.type !== "text")
147
+ return "";
148
+ return typeof record.text === "string" ? record.text : "";
149
+ }).join("\n")
150
+ : "";
151
+ return text.trim().length > 0 ? text : undefined;
152
+ }
153
+ function extractPlanItems(item) {
154
+ if (!Array.isArray(item.items))
155
+ return [];
156
+ return item.items.slice(0, AGENT_PLAN_MAX_ITEMS).flatMap((value) => {
157
+ if (!value || typeof value !== "object")
158
+ return [];
159
+ const record = value;
160
+ if (typeof record.text !== "string" || record.text.trim().length === 0)
161
+ return [];
162
+ return [{
163
+ text: record.text.slice(0, AGENT_PLAN_ITEM_TEXT_MAX_CHARS),
164
+ done: record.completed === true || record.done === true,
165
+ ...(record.active === true ? { active: true } : {}),
166
+ }];
167
+ });
168
+ }
46
169
  function parseSemver(version) {
47
170
  const match = version.trim().match(/(\d+)\.(\d+)\.(\d+)/);
48
171
  if (!match)
@@ -304,15 +427,19 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
304
427
  const resumeThreadId = options.threadId?.trim();
305
428
  let server;
306
429
  let activeTurnId;
430
+ const responseSeenTurnIds = new Set();
431
+ const completedBeforeResponseTurnIds = new Set();
307
432
  let activeTurnSource;
308
433
  let activeTurnAcceptingSteer = false;
309
434
  let activeThreadId;
310
435
  let interruptRequested = false;
311
436
  let lastAgentMessage = "";
312
437
  let builderDiagnostic;
438
+ let emitTurnStarted;
313
439
  const pendingTells = [];
314
440
  let drainingTells = false;
315
441
  let failActiveTurn;
442
+ let finishAfterTellDrain;
316
443
  const rejectPendingTells = (error) => {
317
444
  for (const pending of pendingTells.splice(0))
318
445
  pending.reject(error);
@@ -331,6 +458,15 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
331
458
  }
332
459
  return activeTurnId;
333
460
  };
461
+ const recordTurnStartResponse = (candidate) => {
462
+ const turnId = coerceString(candidate);
463
+ if (!turnId)
464
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} response omitted active turn id`);
465
+ if (completedBeforeResponseTurnIds.delete(turnId))
466
+ return turnId;
467
+ responseSeenTurnIds.add(turnId);
468
+ return recordActiveTurnId(turnId, "response");
469
+ };
334
470
  appendDebugLog(`startCodexAppServer: executable=${executable} model=${options.model ?? "default"} mode=${resumeThreadId ? "resume" : "fresh"}`, { cwd, section: CODEX_APP_SERVER_PROVIDER });
335
471
  const submitSteer = async (tell, turnId) => {
336
472
  // turn/steer response {turnId} = enqueue acceptance (submitted), not consumption.
@@ -346,6 +482,8 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
346
482
  if (acknowledgedTurnId !== turnId) {
347
483
  throw new Error(`${CODEX_APP_SERVER_PROVIDER} turn/steer acknowledgement mismatched active turn: expected=${turnId}, actual=${acknowledgedTurnId}`);
348
484
  }
485
+ // Accepted same-native-turn steer opens the next public conversation turn.
486
+ emitTurnStarted?.();
349
487
  if (options.onEvidence) {
350
488
  emitProviderEvidence(options.onEvidence, {
351
489
  type: "ack",
@@ -361,14 +499,21 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
361
499
  threadId: activeThreadId,
362
500
  input: [{ type: "text", text: tell.text }],
363
501
  ...(options.model ? { model: options.model } : {}),
364
- ...(tell.effort ? { effort: tell.effort } : {}),
365
- ...(!configOwnsApprovalPolicy(options.config) ? { approvalPolicy: CODEX_APP_SERVER_TURN_APPROVAL_POLICY } : {}),
366
- ...(!configOwnsSandboxPolicy(options.config) ? { sandboxPolicy: buildCodexAppServerDefaultSandboxPolicy(cwd) } : {}),
502
+ ...(tell.effort ? { effort: toCodexAppServerEffort(tell.effort) } : {}),
503
+ ...(!configOwnsApprovalPolicy(options.config) ? { approvalPolicy: options.approvalPolicy ?? CODEX_APP_SERVER_TURN_APPROVAL_POLICY } : {}),
504
+ ...(!configOwnsSandboxPolicy(options.config)
505
+ ? {
506
+ sandboxPolicy: options.sandboxPolicy ?? buildCodexAppServerWorkspaceWriteSandboxPolicy({
507
+ writableRoots: [cwd],
508
+ networkAccess: false,
509
+ }),
510
+ }
511
+ : {}),
367
512
  });
368
513
  const startedTurn = started && typeof started === "object"
369
514
  ? (started.turn ?? undefined)
370
515
  : undefined;
371
- const newTurnId = recordActiveTurnId(startedTurn?.id, "response");
516
+ const newTurnId = recordTurnStartResponse(startedTurn?.id);
372
517
  if (options.onEvidence)
373
518
  emitProviderEvidence(options.onEvidence, { type: "ack", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: newTurnId }, outbound: "run" }, builderDiagnostic);
374
519
  return { fence: { kind: "turn", turnId: newTurnId } };
@@ -400,6 +545,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
400
545
  }
401
546
  finally {
402
547
  drainingTells = false;
548
+ const finish = finishAfterTellDrain;
549
+ finishAfterTellDrain = undefined;
550
+ finish?.();
403
551
  }
404
552
  };
405
553
  const enqueue = async (tell) => {
@@ -413,7 +561,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
413
561
  };
414
562
  return createProjectionDriver({
415
563
  steerMode: "mid-execution",
564
+ initialTurn: options.initialTurn,
416
565
  enqueue,
566
+ onIngress: options.onIngress,
417
567
  async abort() {
418
568
  interruptRequested = true;
419
569
  if (server && activeThreadId && activeTurnId) {
@@ -432,6 +582,7 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
432
582
  },
433
583
  async run(builder) {
434
584
  builderDiagnostic = (text) => builder.emit({ type: "diagnostic", text });
585
+ emitTurnStarted = () => builder.emit({ type: "turn", phase: "started" });
435
586
  const env = buildSubagentEnv();
436
587
  server = new CodexAppServerProcess(executable, env, options.codexHome, options.onEvidence, (text) => builder.emit({ type: "diagnostic", text }));
437
588
  const turnCompleted = new Promise((resolve, reject) => {
@@ -456,58 +607,72 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
456
607
  failTurn(new Error(message));
457
608
  };
458
609
  unsubServerRequest = server.onServerRequest((request) => {
459
- failServerRequest(request);
610
+ builder.ingress(CODEX_APP_SERVER_PROVIDER, request.method, request, "mapped", () => {
611
+ failServerRequest(request);
612
+ });
460
613
  });
461
614
  unsubNotifications = server.onNotification((notification) => {
462
615
  try {
463
- builder.emit({ type: "output", value: notification });
464
- if (notification.method === "item/agentMessage/delta") {
465
- const delta = coerceString(notification.params?.delta);
466
- if (delta) {
467
- lastAgentMessage = `${lastAgentMessage}${delta}`;
468
- builder.emit({ type: "message", text: lastAgentMessage });
469
- }
470
- }
471
- if (notification.method === "item/completed") {
472
- const item = notification.params?.item;
473
- if (item && (item.type === "agentMessage" || item.type === "agent_message")) {
474
- const text = coerceString(item.text);
475
- if (text) {
476
- lastAgentMessage = text;
477
- builder.emit({ type: "message", text });
616
+ builder.ingress(CODEX_APP_SERVER_PROVIDER, notification.method, notification, codexAppServerNotificationDisposition(notification.method), () => {
617
+ if (notification.method === "item/agentMessage/delta") {
618
+ const delta = coerceString(notification.params?.delta);
619
+ if (delta) {
620
+ lastAgentMessage = `${lastAgentMessage}${delta}`;
478
621
  }
479
622
  }
480
- }
481
- if (notification.method === "turn/started") {
482
- const turn = notification.params?.turn;
483
- const turnId = recordActiveTurnId(turn?.id, "notification");
484
- activeTurnAcceptingSteer = true;
485
- const checkpoint = { fence: { kind: "turn", turnId } };
486
- if (options.onEvidence) {
487
- emitProviderEvidence(options.onEvidence, {
488
- type: "checkpoint",
489
- provider: CODEX_APP_SERVER_PROVIDER,
490
- ...checkpoint,
491
- source: "notification",
492
- }, builderDiagnostic);
623
+ if (notification.method === "item/completed") {
624
+ const item = notification.params?.item;
625
+ if (!item) {
626
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/completed lacked required item`);
627
+ }
628
+ const id = coerceString(item.id);
629
+ const itemType = coerceString(item.type);
630
+ if (!id || !itemType) {
631
+ throw new Error(`${CODEX_APP_SERVER_PROVIDER} item/completed lacked required item id or type`);
632
+ }
633
+ if (itemType === "agentMessage") {
634
+ const text = coerceString(item.text);
635
+ if (text) {
636
+ lastAgentMessage = text;
637
+ builder.emit({ type: "message", text });
638
+ }
639
+ }
640
+ else if (itemType === "reasoning") {
641
+ const text = extractReasoningSummary(item);
642
+ if (text) {
643
+ const truncated = text.length > AGENT_THOUGHT_TEXT_MAX_CHARS;
644
+ builder.emit({
645
+ type: "thought",
646
+ id,
647
+ text: text.slice(0, AGENT_THOUGHT_TEXT_MAX_CHARS),
648
+ final: true,
649
+ ...(truncated ? { truncated: true } : {}),
650
+ });
651
+ }
652
+ }
653
+ else if (itemType === "todo_list" || itemType === "todoList") {
654
+ builder.emit({
655
+ type: "plan",
656
+ id,
657
+ items: extractPlanItems(item),
658
+ });
659
+ }
660
+ else if (isCodexAppServerActivityItemType(itemType)) {
661
+ builder.emit({
662
+ type: "activity",
663
+ phase: "completed",
664
+ id,
665
+ tool: itemType,
666
+ call: appServerActivityCall(itemType, item),
667
+ result: appServerActivityResult(item),
668
+ });
669
+ }
493
670
  }
494
- // turn/start may respond before the server has installed its active
495
- // turn. The native notification is the first steer-safe boundary.
496
- options.onSyncCheckpoint?.({ enqueue }, checkpoint);
497
- void drainPendingTells();
498
- }
499
- if (notification.method === "turn/completed") {
500
- const turn = notification.params?.turn ?? {};
501
- const status = coerceString(turn.status) ?? "unknown";
502
- // Either response or notification may mint the turn fence first;
503
- // every later native source must agree with it.
504
- const turnId = recordActiveTurnId(turn.id, "notification");
505
- // This must stay in the native notification call stack: the harness
506
- // checkpoint and its observer evidence describe this exact crossing.
507
- if (status === "completed") {
508
- activeTurnAcceptingSteer = false;
509
- activeTurnId = undefined;
510
- activeTurnSource = undefined;
671
+ if (notification.method === "turn/started") {
672
+ const turn = notification.params?.turn;
673
+ const turnId = recordActiveTurnId(turn?.id, "notification");
674
+ builder.emit({ type: "turn", phase: "started" });
675
+ activeTurnAcceptingSteer = true;
511
676
  const checkpoint = { fence: { kind: "turn", turnId } };
512
677
  if (options.onEvidence) {
513
678
  emitProviderEvidence(options.onEvidence, {
@@ -517,23 +682,63 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
517
682
  source: "notification",
518
683
  }, builderDiagnostic);
519
684
  }
685
+ // turn/start may respond before the server has installed its active
686
+ // turn. The native notification is the first steer-safe boundary.
520
687
  options.onSyncCheckpoint?.({ enqueue }, checkpoint);
521
688
  void drainPendingTells();
522
- if (pendingTells.length > 0 || drainingTells) {
523
- return;
689
+ }
690
+ if (notification.method === "turn/completed") {
691
+ const turn = notification.params?.turn ?? {};
692
+ const status = coerceString(turn.status) ?? "unknown";
693
+ // Either response or notification may mint the turn fence first;
694
+ // every later native source must agree with it.
695
+ const turnId = recordActiveTurnId(turn.id, "notification");
696
+ if (!responseSeenTurnIds.delete(turnId)) {
697
+ completedBeforeResponseTurnIds.add(turnId);
524
698
  }
699
+ // Public completed only for exact native status completed.
700
+ if (status === "completed") {
701
+ builder.emit({ type: "turn", phase: "completed" });
702
+ // This must stay in the native notification call stack: the harness
703
+ // checkpoint and its observer evidence describe this exact crossing.
704
+ activeTurnAcceptingSteer = false;
705
+ activeTurnId = undefined;
706
+ activeTurnSource = undefined;
707
+ const checkpoint = { fence: { kind: "turn", turnId } };
708
+ if (options.onEvidence) {
709
+ emitProviderEvidence(options.onEvidence, {
710
+ type: "checkpoint",
711
+ provider: CODEX_APP_SERVER_PROVIDER,
712
+ ...checkpoint,
713
+ source: "notification",
714
+ }, builderDiagnostic);
715
+ }
716
+ options.onSyncCheckpoint?.({ enqueue }, checkpoint);
717
+ void drainPendingTells();
718
+ if (pendingTells.length > 0 || drainingTells) {
719
+ finishAfterTellDrain = () => {
720
+ if (settled || activeTurnId !== undefined || pendingTells.length > 0)
721
+ return;
722
+ settled = true;
723
+ unsubNotifications();
724
+ unsubServerRequest();
725
+ resolve({ status, turn });
726
+ };
727
+ return;
728
+ }
729
+ }
730
+ if (settled)
731
+ return;
732
+ settled = true;
733
+ unsubNotifications();
734
+ unsubServerRequest();
735
+ resolve({ status, turn });
525
736
  }
526
- if (settled)
527
- return;
528
- settled = true;
529
- unsubNotifications();
530
- unsubServerRequest();
531
- resolve({ status, turn });
532
- }
533
- if (notification.method === "error") {
534
- const message = coerceString(notification.params?.message) ?? `${CODEX_APP_SERVER_PROVIDER} error`;
535
- builder.emit({ type: "diagnostic", text: message });
536
- }
737
+ if (notification.method === "error") {
738
+ const message = coerceString(notification.params?.message) ?? `${CODEX_APP_SERVER_PROVIDER} error`;
739
+ builder.emit({ type: "diagnostic", text: message });
740
+ }
741
+ });
537
742
  }
538
743
  catch (error) {
539
744
  // Notification listeners run outside the driver's await chain;
@@ -601,13 +806,16 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
601
806
  threadId: activeThreadId,
602
807
  input: [{ type: "text", text: prompt }],
603
808
  ...(options.model ? { model: options.model } : {}),
604
- ...(options.effort ? { effort: options.effort } : {}),
809
+ ...(options.effort ? { effort: toCodexAppServerEffort(options.effort) } : {}),
605
810
  };
606
811
  if (!configOwnsApprovalPolicy(options.config)) {
607
- turnStartParams.approvalPolicy = CODEX_APP_SERVER_TURN_APPROVAL_POLICY;
812
+ turnStartParams.approvalPolicy = options.approvalPolicy ?? CODEX_APP_SERVER_TURN_APPROVAL_POLICY;
608
813
  }
609
814
  if (!configOwnsSandboxPolicy(options.config)) {
610
- turnStartParams.sandboxPolicy = buildCodexAppServerDefaultSandboxPolicy(cwd);
815
+ turnStartParams.sandboxPolicy = options.sandboxPolicy ?? buildCodexAppServerWorkspaceWriteSandboxPolicy({
816
+ writableRoots: [cwd],
817
+ networkAccess: false,
818
+ });
611
819
  }
612
820
  activeTurnId = undefined;
613
821
  activeTurnSource = undefined;
@@ -616,9 +824,9 @@ export async function startCodexAppServer(prompt, cwd, options = {}) {
616
824
  const turn = turnStart && typeof turnStart === "object"
617
825
  ? (turnStart.turn ?? undefined)
618
826
  : undefined;
619
- recordActiveTurnId(turn?.id, "response");
827
+ const turnId = recordTurnStartResponse(turn?.id);
620
828
  if (options.onEvidence)
621
- emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId: activeTurnId }, source: "response" }, builderDiagnostic);
829
+ emitProviderEvidence(options.onEvidence, { type: "checkpoint", provider: CODEX_APP_SERVER_PROVIDER, fence: { kind: "turn", turnId }, source: "response" }, builderDiagnostic);
622
830
  const completed = await turnCompleted;
623
831
  if (completed.status === "interrupted" || interruptRequested) {
624
832
  throw createAbortError(`${CODEX_APP_SERVER_PROVIDER} turn interrupted`);