@goah/cli 0.11.4 → 0.13.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.
Files changed (160) hide show
  1. package/dist/cli.js +165 -66
  2. package/dist/cli.js.map +1 -1
  3. package/dist/console/assets/index-3sYrDMmt.css +1 -0
  4. package/dist/console/assets/index-Rtc_B7cs.js +52 -0
  5. package/dist/console/assets/index-Rtc_B7cs.js.map +1 -0
  6. package/dist/console/index.html +2 -2
  7. package/dist/control.d.ts +13 -13
  8. package/dist/control.d.ts.map +1 -1
  9. package/dist/control.js +77 -50
  10. package/dist/control.js.map +1 -1
  11. package/dist/index.d.ts +6 -23
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +156 -28
  14. package/dist/index.js.map +1 -1
  15. package/dist/live-config.js +2 -2
  16. package/dist/live-config.js.map +1 -1
  17. package/dist/prompt-queue.d.ts.map +1 -1
  18. package/dist/prompt-queue.js +5 -0
  19. package/dist/prompt-queue.js.map +1 -1
  20. package/dist/setup-wizard.d.ts +13 -2
  21. package/dist/setup-wizard.d.ts.map +1 -1
  22. package/dist/setup-wizard.js +87 -63
  23. package/dist/setup-wizard.js.map +1 -1
  24. package/dist/thread-inspect.d.ts +47 -0
  25. package/dist/thread-inspect.d.ts.map +1 -0
  26. package/dist/thread-inspect.js +78 -0
  27. package/dist/thread-inspect.js.map +1 -0
  28. package/dist/transcript.d.ts +2 -0
  29. package/dist/transcript.d.ts.map +1 -0
  30. package/dist/transcript.js +2 -0
  31. package/dist/transcript.js.map +1 -0
  32. package/dist/tui-theme.d.ts +1 -0
  33. package/dist/tui-theme.d.ts.map +1 -1
  34. package/dist/tui-theme.js +1 -0
  35. package/dist/tui-theme.js.map +1 -1
  36. package/dist/tui.d.ts +16 -3
  37. package/dist/tui.d.ts.map +1 -1
  38. package/dist/tui.js +332 -80
  39. package/dist/tui.js.map +1 -1
  40. package/dist/update.d.ts +1 -0
  41. package/dist/update.d.ts.map +1 -1
  42. package/dist/update.js +15 -7
  43. package/dist/update.js.map +1 -1
  44. package/dist/web-console.d.ts +1 -1
  45. package/dist/web-console.d.ts.map +1 -1
  46. package/dist/web-console.js +27 -37
  47. package/dist/web-console.js.map +1 -1
  48. package/dist/welcome.d.ts +2 -2
  49. package/dist/welcome.d.ts.map +1 -1
  50. package/dist/welcome.js +24 -36
  51. package/dist/welcome.js.map +1 -1
  52. package/node_modules/goah-ledger-contract/dist/execution.d.ts +272 -130
  53. package/node_modules/goah-ledger-contract/dist/execution.d.ts.map +1 -1
  54. package/node_modules/goah-ledger-contract/dist/execution.js +31 -19
  55. package/node_modules/goah-ledger-contract/dist/execution.js.map +1 -1
  56. package/node_modules/goah-ledger-contract/dist/index.d.ts +1 -2
  57. package/node_modules/goah-ledger-contract/dist/index.d.ts.map +1 -1
  58. package/node_modules/goah-ledger-contract/dist/index.js +1 -2
  59. package/node_modules/goah-ledger-contract/dist/index.js.map +1 -1
  60. package/node_modules/goah-ledger-contract/dist/kernel.d.ts +1 -1
  61. package/node_modules/goah-ledger-contract/dist/kernel.d.ts.map +1 -1
  62. package/node_modules/goah-ledger-contract/dist/kernel.js +1 -1
  63. package/node_modules/goah-ledger-contract/dist/kernel.js.map +1 -1
  64. package/node_modules/goah-ledger-contract/dist/transcript.d.ts +63 -0
  65. package/node_modules/goah-ledger-contract/dist/transcript.d.ts.map +1 -0
  66. package/node_modules/goah-ledger-contract/dist/transcript.js +115 -0
  67. package/node_modules/goah-ledger-contract/dist/transcript.js.map +1 -0
  68. package/node_modules/goah-ledger-contract/package.json +3 -4
  69. package/node_modules/goah-ledger-sqlite/dist/index.d.ts +32 -23
  70. package/node_modules/goah-ledger-sqlite/dist/index.d.ts.map +1 -1
  71. package/node_modules/goah-ledger-sqlite/dist/index.js +764 -394
  72. package/node_modules/goah-ledger-sqlite/dist/index.js.map +1 -1
  73. package/node_modules/goah-ledger-sqlite/package.json +2 -2
  74. package/node_modules/goah-runner-pi/dist/configurator.d.ts.map +1 -1
  75. package/node_modules/goah-runner-pi/dist/configurator.js +186 -30
  76. package/node_modules/goah-runner-pi/dist/configurator.js.map +1 -1
  77. package/node_modules/goah-runner-pi/dist/credential-store.d.ts +2 -0
  78. package/node_modules/goah-runner-pi/dist/credential-store.d.ts.map +1 -1
  79. package/node_modules/goah-runner-pi/dist/credential-store.js +117 -23
  80. package/node_modules/goah-runner-pi/dist/credential-store.js.map +1 -1
  81. package/node_modules/goah-runner-pi/dist/index.d.ts +18 -6
  82. package/node_modules/goah-runner-pi/dist/index.d.ts.map +1 -1
  83. package/node_modules/goah-runner-pi/dist/index.js +97 -12
  84. package/node_modules/goah-runner-pi/dist/index.js.map +1 -1
  85. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts +6 -2
  86. package/node_modules/goah-runner-pi/dist/pi-worker.d.ts.map +1 -1
  87. package/node_modules/goah-runner-pi/dist/pi-worker.js +232 -70
  88. package/node_modules/goah-runner-pi/dist/pi-worker.js.map +1 -1
  89. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts +2 -0
  90. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.d.ts.map +1 -0
  91. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js +3 -0
  92. package/node_modules/goah-runner-pi/dist/steering-no-ack.test-fixture.js.map +1 -0
  93. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts +2 -0
  94. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.d.ts.map +1 -0
  95. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js +5 -0
  96. package/node_modules/goah-runner-pi/dist/steering-reject.test-fixture.js.map +1 -0
  97. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts +2 -0
  98. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.d.ts.map +1 -0
  99. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js +5 -0
  100. package/node_modules/goah-runner-pi/dist/steering-worker.test-fixture.js.map +1 -0
  101. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts +1 -1
  102. package/node_modules/goah-runner-pi/dist/test-helpers.d.ts.map +1 -1
  103. package/node_modules/goah-runner-pi/dist/test-helpers.js +3 -1
  104. package/node_modules/goah-runner-pi/dist/test-helpers.js.map +1 -1
  105. package/node_modules/goah-runner-pi/dist/verification-worker.d.ts.map +1 -1
  106. package/node_modules/goah-runner-pi/dist/verification-worker.js +3 -5
  107. package/node_modules/goah-runner-pi/dist/verification-worker.js.map +1 -1
  108. package/node_modules/goah-runner-pi/package.json +2 -2
  109. package/node_modules/goah-supervisor/dist/context-view.d.ts +4 -5
  110. package/node_modules/goah-supervisor/dist/context-view.d.ts.map +1 -1
  111. package/node_modules/goah-supervisor/dist/context-view.js +7 -20
  112. package/node_modules/goah-supervisor/dist/context-view.js.map +1 -1
  113. package/node_modules/goah-supervisor/dist/index.d.ts +38 -36
  114. package/node_modules/goah-supervisor/dist/index.d.ts.map +1 -1
  115. package/node_modules/goah-supervisor/dist/index.js +762 -575
  116. package/node_modules/goah-supervisor/dist/index.js.map +1 -1
  117. package/node_modules/goah-supervisor/dist/roles.d.ts +2 -1
  118. package/node_modules/goah-supervisor/dist/roles.d.ts.map +1 -1
  119. package/node_modules/goah-supervisor/dist/roles.js +20 -5
  120. package/node_modules/goah-supervisor/dist/roles.js.map +1 -1
  121. package/node_modules/goah-supervisor/dist/verification.d.ts +8 -29
  122. package/node_modules/goah-supervisor/dist/verification.d.ts.map +1 -1
  123. package/node_modules/goah-supervisor/dist/verification.js +71 -46
  124. package/node_modules/goah-supervisor/dist/verification.js.map +1 -1
  125. package/node_modules/goah-supervisor/package.json +2 -2
  126. package/node_modules/goah-testkit/dist/faux-runner-worker.js +16 -9
  127. package/node_modules/goah-testkit/dist/faux-runner-worker.js.map +1 -1
  128. package/node_modules/goah-testkit/dist/index.d.ts +13 -16
  129. package/node_modules/goah-testkit/dist/index.d.ts.map +1 -1
  130. package/node_modules/goah-testkit/dist/index.js +27 -44
  131. package/node_modules/goah-testkit/dist/index.js.map +1 -1
  132. package/node_modules/goah-testkit/package.json +5 -5
  133. package/package.json +7 -8
  134. package/dist/console/assets/index-BJTGvlCH.js +0 -52
  135. package/dist/console/assets/index-BJTGvlCH.js.map +0 -1
  136. package/dist/console/assets/index-msBn41Cl.css +0 -1
  137. package/dist/inspect.d.ts +0 -61
  138. package/dist/inspect.d.ts.map +0 -1
  139. package/dist/inspect.js +0 -110
  140. package/dist/inspect.js.map +0 -1
  141. package/dist/metrics.d.ts +0 -2
  142. package/dist/metrics.d.ts.map +0 -1
  143. package/dist/metrics.js +0 -2
  144. package/dist/metrics.js.map +0 -1
  145. package/dist/session.d.ts +0 -2
  146. package/dist/session.d.ts.map +0 -1
  147. package/dist/session.js +0 -2
  148. package/dist/session.js.map +0 -1
  149. package/node_modules/goah-ledger-contract/dist/metrics.d.ts +0 -40
  150. package/node_modules/goah-ledger-contract/dist/metrics.d.ts.map +0 -1
  151. package/node_modules/goah-ledger-contract/dist/metrics.js +0 -25
  152. package/node_modules/goah-ledger-contract/dist/metrics.js.map +0 -1
  153. package/node_modules/goah-ledger-contract/dist/session.d.ts +0 -63
  154. package/node_modules/goah-ledger-contract/dist/session.d.ts.map +0 -1
  155. package/node_modules/goah-ledger-contract/dist/session.js +0 -115
  156. package/node_modules/goah-ledger-contract/dist/session.js.map +0 -1
  157. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts +0 -2
  158. package/node_modules/goah-testkit/dist/mock-connector-worker.d.ts.map +0 -1
  159. package/node_modules/goah-testkit/dist/mock-connector-worker.js +0 -29
  160. package/node_modules/goah-testkit/dist/mock-connector-worker.js.map +0 -1
@@ -1,8 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { capabilityFor, controlStream, evaluateMetric, goalStream, interruptedSessionEvents, memoryStream, wakeStream, } from "goah-ledger-contract";
2
+ import { assertAgentHandoff, assertHandoff, assertTurnOutput, goalStream, goalRoute, goalExecutionBinding, goalTurnBinding, humanInboxRoute, humanRequestRoute, humanBinding, humanTurnBinding, specialistBinding, specialistTurnBinding, memoryStream, wakeStream, } from "goah-ledger-contract";
4
3
  import { composeActiveContext, selectRecoveryEvents, selectWorkingMemory } from "./context-view.js";
5
- import { defaultRolePrompt } from "./roles.js";
4
+ import { defaultTurnPrompt } from "./roles.js";
6
5
  export { composeActiveContext, selectRecoveryEvents } from "./context-view.js";
7
6
  /** Dispatches each wake to the runner selected by its opaque Runner Profile. */
8
7
  export class RunnerRouter {
@@ -24,12 +23,13 @@ export class RunnerRouter {
24
23
  const handle = runner.prepare(request);
25
24
  if (handle.pid)
26
25
  this.#byPid.set(handle.pid, runner);
27
- void handle.result.finally(() => { if (handle.pid)
26
+ void handle.result.then(() => { if (handle.pid)
27
+ this.#byPid.delete(handle.pid); }, () => { if (handle.pid)
28
28
  this.#byPid.delete(handle.pid); });
29
29
  return handle;
30
30
  }
31
- async terminateProcess(pid) {
32
- const runner = this.#byPid.get(pid);
31
+ async terminateProcess(pid, runnerProfileId) {
32
+ const runner = this.#byPid.get(pid) ?? this.runners.get(runnerProfileId ?? this.fallback);
33
33
  if (!runner)
34
34
  return;
35
35
  await runner.terminateProcess(pid);
@@ -40,19 +40,17 @@ export class Supervisor {
40
40
  clock;
41
41
  #leaseMs;
42
42
  #memoryTailChars;
43
- #allowExternalActions;
44
- #approvers;
45
- #auditWriters;
46
43
  #claimTail = Promise.resolve();
47
- #connectors = new Map();
48
- #metricCollectors = new Map();
49
- #metricContracts = new Map();
50
- #silence;
44
+ #humanTail = Promise.resolve();
51
45
  #retryPolicy;
46
+ #turnRetryPolicy;
52
47
  #profiles;
53
48
  #runnerProfiles;
54
49
  #handles = new Map();
55
- #verifyMetricsAfterWake;
50
+ #executions = new Map();
51
+ #agentExecutions = new Map();
52
+ #handoffValidations = new Map();
53
+ #handoffAttemptSeq = new Map();
56
54
  #runner;
57
55
  constructor(ledger, runner, clock, options = {}) {
58
56
  this.ledger = ledger;
@@ -60,31 +58,25 @@ export class Supervisor {
60
58
  this.#runner = runner;
61
59
  this.#leaseMs = options.leaseMs ?? 30_000;
62
60
  this.#memoryTailChars = options.memoryTailChars ?? 12_000;
63
- this.#allowExternalActions = options.allowExternalActions ?? false;
64
- this.#approvers = new Set(options.approvers ?? ["human", "ceo"]);
65
- this.#auditWriters = new Set(options.auditWriters ?? ["verifier", "audit"]);
66
- this.#silence = options.silence === null ? null : { maxSilentMs: options.silence?.maxSilentMs ?? 12 * 3_600_000, notify: options.silence?.notify ?? "ceo" };
67
61
  this.#retryPolicy = options.retryPolicy ?? { maxAttempts: 0, baseDelayMs: 1_000 };
62
+ this.#turnRetryPolicy = options.turnRetryPolicy ?? { maxAttempts: 3, baseDelayMs: 1_000 };
63
+ for (const profile of options.profiles ?? []) {
64
+ if (profile.agent === "ceo" && profile.role !== "ceo" || profile.agent !== "ceo" && profile.role === "ceo")
65
+ throw new Error("exactly the ceo Agent must use the ceo role");
66
+ }
68
67
  this.#profiles = new Map([["ceo", { agent: "ceo", role: "ceo" }], ...(options.profiles ?? []).map((profile) => [profile.agent, profile])]);
69
68
  this.#runnerProfiles = new Map((options.runnerProfiles ?? []).map((profile) => [profile.id, profile]));
70
- this.#verifyMetricsAfterWake = options.verifyMetricsAfterWake ?? false;
71
- }
72
- registerConnector(connector) { this.#connectors.set(connector.manifest.connector, connector); }
73
- registerMetricContract(goalId, contract) { this.#metricContracts.set(goalId, contract); }
74
- registerMetricCollector(goalId, contract, spec, intervalMs = 60_000) {
75
- this.registerMetricContract(goalId, contract);
76
- this.#metricCollectors.set(goalId, { goalId, contract, spec, intervalMs, nextAt: 0 });
77
69
  }
78
70
  /**
79
- * Hot-swap the runner after a config reload. Refused while a wake is leased
80
- * or running: a live child must never observe its runner vanish mid-flight.
81
- * The next tick claims wakes through the new runner, and spawn-time env
71
+ * Hot-swap the runner after a config reload. Refused while a Turn is active:
72
+ * a live child must never observe its runner vanish mid-flight.
73
+ * The next Turn uses the new runner, and spawn-time env
82
74
  * resolution applies the new credentials to the next spawn.
83
75
  */
84
76
  swapRunner(runner, profiles) {
85
- const active = this.ledger.wakes().filter((wake) => wake.status === "leased" || wake.status === "running");
77
+ const active = this.ledger.turns().filter((turn) => turn.status === "in_progress");
86
78
  if (active.length > 0)
87
- throw new Error("cannot swap runner while a wake is leased or running");
79
+ throw new Error("cannot swap runner while a Turn is in progress");
88
80
  this.#runner = runner;
89
81
  if (profiles) {
90
82
  this.#runnerProfiles.clear();
@@ -93,48 +85,330 @@ export class Supervisor {
93
85
  }
94
86
  }
95
87
  get runner() { return this.#runner; }
96
- createGoal(goal, actor = "human") { this.ledger.putGoal(goal, actor); }
97
- createRootGoal(objective, id = randomUUID()) {
88
+ createGoal(goal, actor = "human") { if (!this.#validGoalOwner(goal.owner, goal))
89
+ throw new Error("Goal owner role does not match Root/Child position"); if (goal.parentId === null && this.ledger.goals().some((candidate) => candidate.id !== goal.id && candidate.parentId === null && candidate.phase !== "complete"))
90
+ throw new Error("CEO already has an unfinished Root Goal"); this.ledger.putGoal(goal, actor); }
91
+ createRootGoal(objective, id = randomUUID(), sourceTurnId) {
98
92
  if (!objective.trim())
99
93
  throw new Error("root objective is required");
100
94
  if (this.ledger.goals().some((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase !== "complete"))
101
95
  throw new Error("CEO already has an unfinished Root Goal; use work_on_goal");
102
96
  const goal = { id, parentId: null, objective, observationMethod: null, verificationMethod: null, owner: "ceo", phase: "active", revision: 0 };
103
- this.ledger.putGoal(goal, "human");
97
+ this.ledger.putGoal(goal, "human", undefined, { operation: "create", reason: "Human established a durable Root Goal", evidence: [], authority: { kind: "human" }, ...(sourceTurnId ? { sourceTurnId } : {}) });
104
98
  return this.#goal(id);
105
99
  }
106
100
  startGoal(objective, id = randomUUID()) {
107
101
  const goal = this.createRootGoal(objective, id);
108
- const wake = this.#enqueueTrigger("ceo", `root:${id}:created`);
102
+ const wake = this.#enqueueTrigger("ceo", `root:${id}:created`, { goalId: goal.id });
109
103
  if (!wake)
110
104
  throw new Error("CEO wake was not admitted for an active root goal");
111
105
  return { goal, wake };
112
106
  }
113
- interactWithCeo(message) {
107
+ threadFor(agent = "ceo") {
108
+ const parentThreadId = agent === "ceo" ? null : this.threadFor("ceo").id;
109
+ const existing = this.ledger.threads().find((thread) => thread.agent === agent && thread.parentThreadId === parentThreadId);
110
+ if (existing)
111
+ return existing;
112
+ const now = this.#now();
113
+ const thread = { id: randomUUID(), agent, parentThreadId, createdAt: now, updatedAt: now };
114
+ this.ledger.putThread(thread, "supervisor");
115
+ return thread;
116
+ }
117
+ async startHumanTurn(message) {
118
+ const previous = this.#humanTail;
119
+ let release;
120
+ this.#humanTail = new Promise((resolve) => { release = resolve; });
121
+ await previous;
122
+ try {
123
+ return await this.#startHumanTurn(message);
124
+ }
125
+ finally {
126
+ release();
127
+ }
128
+ }
129
+ async #startHumanTurn(message) {
114
130
  if (!message.trim())
115
131
  throw new Error("message is required");
116
- const mail = { id: randomUUID(), to: "ceo", from: "human", level: "fyi", body: { type: "interaction", message }, readAt: null };
117
- this.ledger.putMail(mail, "human");
118
- const wake = this.#enqueueTrigger("ceo", `interaction:${mail.id}`);
119
- if (!wake)
120
- throw new Error("CEO interaction was not admitted");
121
- return { mail, wake };
132
+ const thread = this.threadFor("ceo");
133
+ const active = this.ledger.activeTurn(thread.id);
134
+ if (active) {
135
+ if (active.source !== "human") {
136
+ const goalId = active.goalId;
137
+ await this.interruptTurn(active.id);
138
+ if (goalId) {
139
+ const goal = this.ledger.goal(goalId);
140
+ if (goal?.phase === "active")
141
+ this.#enqueueTrigger(goal.owner, `human-interrupted:${active.id}`, { goalId: goal.id });
142
+ }
143
+ }
144
+ else {
145
+ const handle = this.#handles.get(active.id);
146
+ if (handle && !handle.steer)
147
+ await this.interruptTurn(active.id);
148
+ else if (handle?.steer) {
149
+ const item = this.#appendTurnItem(active.id, "user_message", { text: message }, "human", randomUUID(), "in_progress");
150
+ try {
151
+ await handle.steer(message);
152
+ const stored = this.ledger.turnItems(active.id).find((candidate) => candidate.id === item.id);
153
+ if (this.ledger.turn(active.id)?.status === "in_progress" && stored?.status === "in_progress") {
154
+ this.ledger.putTurnItem({ ...stored, status: "completed", completedAt: this.#now() }, "human");
155
+ return { threadId: thread.id, turnId: active.id, steered: true };
156
+ }
157
+ }
158
+ catch {
159
+ const stored = this.ledger.turnItems(active.id).find((candidate) => candidate.id === item.id);
160
+ if (this.ledger.turn(active.id)?.status === "in_progress" && stored?.status === "in_progress")
161
+ this.ledger.putTurnItem({ ...stored, status: "failed", completedAt: this.#now() }, "human");
162
+ if (this.ledger.turn(active.id)?.status === "in_progress")
163
+ await this.interruptTurn(active.id);
164
+ }
165
+ }
166
+ else {
167
+ this.#appendTurnItem(active.id, "user_message", { text: message }, "human");
168
+ return { threadId: thread.id, turnId: active.id, steered: true };
169
+ }
170
+ }
171
+ }
172
+ await this.#awaitAgentExecution("ceo");
173
+ if (this.ledger.activeTurn(thread.id))
174
+ return this.#startHumanTurn(message);
175
+ const now = this.#now();
176
+ this.ledger.putThread({ ...thread, updatedAt: now }, "supervisor");
177
+ const leaseToken = randomUUID();
178
+ const turn = { id: randomUUID(), threadId: thread.id, source: "human", ...humanTurnBinding(), status: "in_progress", attempt: 1, error: null, startedAt: now, endedAt: null, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), leaseToken, runnerPid: null, runnerProfileId: this.#runnerProfileId("ceo") };
179
+ this.ledger.putTurn(turn, "human");
180
+ this.#appendTurnItem(turn.id, "user_message", { text: message }, "human");
181
+ const context = { source: { kind: "human" } };
182
+ const deliveredMail = this.#contextMail(humanBinding());
183
+ void this.#trackExecution(turn, "ceo", context, () => this.#humanContext(turn.id, "ceo", deliveredMail), null, deliveredMail.map((mail) => mail.id));
184
+ return { threadId: thread.id, turnId: turn.id, steered: false };
185
+ }
186
+ async interruptTurn(turnId) {
187
+ const turn = this.ledger.turn(turnId);
188
+ if (!turn || turn.status !== "in_progress")
189
+ throw new Error("active Turn not found");
190
+ this.#clearHandoffState(turn.id);
191
+ this.ledger.finishTurn(turn.id, "interrupted", { message: "interrupted by Human" }, this.#now(), "human");
192
+ const handle = this.#handles.get(turn.id);
193
+ try {
194
+ await handle?.terminate();
195
+ }
196
+ catch (error) {
197
+ this.#recordCleanupFailure(turn.id, error);
198
+ }
199
+ finally {
200
+ this.#handles.delete(turn.id);
201
+ }
202
+ await this.#executions.get(turn.id);
203
+ return this.ledger.turn(turn.id);
204
+ }
205
+ async #executeTurn(initial, agent, turnContext, contextFactory, sourceWake = null, deliveredMailIds = [], recordRevisionAtStart = -1, sourceWakeTriggers = []) {
206
+ while (this.ledger.turn(initial.id)?.status === "in_progress") {
207
+ const execution = this.ledger.turn(initial.id);
208
+ const leaseToken = execution.leaseToken;
209
+ let handle = null;
210
+ let renewal = null;
211
+ try {
212
+ handle = this.runner.prepare({ agent, execution, ...(sourceWake ? { sourceWake, sourceWakeTriggers } : {}), turn: turnContext, context: contextFactory(), now: () => this.#now(), emit: (trace) => this.#recordTurnTrace(initial.id, leaseToken, trace.type, trace.data, agent), rpc: (method, params) => this.#agentRpcForTurn(initial.id, agent, turnContext, method, params, sourceWake?.id) });
213
+ this.#handles.set(initial.id, handle);
214
+ if (handle.pid)
215
+ this.ledger.attachTurnProcess(initial.id, leaseToken, handle.pid);
216
+ renewal = setInterval(() => { try {
217
+ this.ledger.renewTurnLease(initial.id, leaseToken, new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), this.#now());
218
+ }
219
+ catch {
220
+ void this.#terminateHandle(initial.id, handle);
221
+ } }, Math.max(25, Math.floor(this.#leaseMs / 3)));
222
+ renewal.unref();
223
+ handle.begin();
224
+ const result = await handle.result;
225
+ if (renewal)
226
+ clearInterval(renewal);
227
+ renewal = null;
228
+ await this.#terminateHandle(initial.id, handle);
229
+ this.#handles.delete(initial.id);
230
+ const current = this.ledger.turn(initial.id);
231
+ if (!current || current.status !== "in_progress")
232
+ return;
233
+ if (current.attempt > 1)
234
+ this.ledger.appendEvent({ streamId: `turn:${current.id}`, ts: this.#now(), actor: "supervisor", type: "turn.retry_finished", data: { attempt: current.attempt, outcome: result.outcome }, ignorable: true });
235
+ if (result.outcome !== "abnormal")
236
+ this.#closeStreamingItems(current.id);
237
+ if (result.outcome === "abnormal") {
238
+ if (current.attempt < this.#turnRetryPolicy.maxAttempts) {
239
+ this.ledger.repairTurnAttempt(current.id, result.reason, this.#now(), "supervisor");
240
+ this.ledger.appendEvent({ streamId: `turn:${current.id}`, ts: this.#now(), actor: "supervisor", type: "turn.retry_started", data: { attempt: current.attempt + 1, reason: result.reason }, ignorable: true });
241
+ this.ledger.putTurn({ ...current, attempt: current.attempt + 1, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), runnerPid: null }, "supervisor");
242
+ await new Promise((resolve) => setTimeout(resolve, this.#turnRetryPolicy.baseDelayMs * 2 ** (current.attempt - 1)));
243
+ continue;
244
+ }
245
+ this.#failTurn(current.id, result.reason);
246
+ this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
247
+ return;
248
+ }
249
+ if (turnContext.goalBinding) {
250
+ if (result.outcome !== "handoff")
251
+ throw new Error("Goal-bound Turn requires Handoff");
252
+ this.#commitGoalTurn(current.id, agent, turnContext, result.output, sourceWake?.id ?? null, deliveredMailIds, recordRevisionAtStart);
253
+ return;
254
+ }
255
+ if (result.outcome !== "response")
256
+ throw new Error("ordinary Turn cannot finish with Handoff");
257
+ if (!this.ledger.turnItems(current.id).some((item) => item.type === "assistant_message" && item.data.text === result.response.content))
258
+ this.#appendTurnItem(current.id, "assistant_message", { text: result.response.content }, agent);
259
+ this.ledger.finishTurn(current.id, "completed", null, this.#now(), "supervisor", deliveredMailIds);
260
+ return;
261
+ }
262
+ catch (error) {
263
+ if (renewal)
264
+ clearInterval(renewal);
265
+ if (handle)
266
+ await this.#terminateHandle(initial.id, handle);
267
+ this.#handles.delete(initial.id);
268
+ const current = this.ledger.turn(initial.id);
269
+ if (current?.status === "in_progress") {
270
+ this.#failTurn(current.id, error instanceof Error ? error.message : String(error));
271
+ this.#scheduleTurnRecovery(sourceWake, sourceWakeTriggers, current.id, agent);
272
+ }
273
+ return;
274
+ }
275
+ }
276
+ }
277
+ #trackExecution(initial, agent, turnContext, contextFactory, sourceWake = null, deliveredMailIds = [], recordRevisionAtStart = -1, sourceWakeTriggers = []) { const running = this.#executeTurn(initial, agent, turnContext, contextFactory, sourceWake, deliveredMailIds, recordRevisionAtStart, sourceWakeTriggers); this.#executions.set(initial.id, running); this.#agentExecutions.set(agent, running); void running.finally(() => { if (this.#executions.get(initial.id) === running)
278
+ this.#executions.delete(initial.id); if (this.#agentExecutions.get(agent) === running)
279
+ this.#agentExecutions.delete(agent); }); return running; }
280
+ async #awaitAgentExecution(agent) { const running = this.#agentExecutions.get(agent); if (running)
281
+ await running; }
282
+ #humanContext(turnId, agent, mail) {
283
+ const turn = this.ledger.turn(turnId);
284
+ const current = this.#turnHistory(turn.id);
285
+ const turnSourceSeqs = this.ledger.readStream(`turn:${turn.id}`).filter((event) => event.type === "item.user_message.started").map((event) => event.seq);
286
+ if (turn.bindingKind === "goal") {
287
+ const context = { source: { kind: "human" }, goalBinding: { goalId: turn.goalId, goalRevision: turn.goalRevision } };
288
+ const base = this.#loadContext({ ...goalExecutionBinding(agent, turn.goalId), attempt: turn.attempt }, context, mail, []);
289
+ const sourceSeqs = Array.isArray(base.sourceSeqs) ? base.sourceSeqs.filter((value) => typeof value === "number") : [];
290
+ return { ...base, text: `${String(base.text ?? "")}\n\n# Current Turn\n\n${current}`, sourceSeqs: [...new Set([...sourceSeqs, ...turnSourceSeqs])].sort((a, b) => a - b) };
291
+ }
292
+ const profile = this.#profiles.get(agent) ?? { agent, role: "ceo" };
293
+ const runnerProfile = this.#runnerProfiles.get(profile.runnerProfile ?? "default");
294
+ const recent = this.ledger.turns(turn.threadId).filter((candidate) => candidate.id !== turn.id && candidate.status === "completed").slice(-8).flatMap((candidate) => this.ledger.turnItems(candidate.id).filter((item) => item.type === "user_message" || item.type === "assistant_message").map((item) => `${item.type === "user_message" ? "Human" : "Assistant"}: ${String(item.data.text ?? "")}`));
295
+ const sourceSeqs = [...turnSourceSeqs, ...this.#mailSourceSeqs(mail)];
296
+ const incoming = mail.map((item) => `- [${item.level}] ${item.id} from ${item.from}: ${boundedJson(item.body)}`);
297
+ const context = { source: { kind: "human" } };
298
+ return { text: [...(recent.length ? [`# Recent conversation\n\n${recent.join("\n")}`] : []), ...(incoming.length ? [`# Incoming\n\n${incoming.join("\n")}`] : []), `# Current Turn\n\n${current}`].join("\n\n"), sourceSeqs, capabilities: profile.capabilities ?? defaultCapabilities(profile.role), systemPrompt: profile.systemPrompt ?? defaultTurnPrompt(profile.role, agent, context), ...(runnerProfile ? { runnerProfile } : {}) };
299
+ }
300
+ #goalContext(wake, turn, turnId, mail, wakeTriggers) { const base = this.#loadContext(wake, turn, mail, wakeTriggers); if (!base || typeof base !== "object" || Array.isArray(base))
301
+ return base; const value = base; const history = this.#turnHistory(turnId); return { ...value, ...(history ? { text: `${String(value.text ?? "")}\n\n# Current Turn retry history\n\n${history}` } : {}) }; }
302
+ #threadAgent(threadId) { const thread = this.ledger.thread(threadId); if (!thread)
303
+ throw new Error("Turn Thread not found"); return thread.agent; }
304
+ #turnHistory(turnId) { const items = this.ledger.turnItems(turnId).map((item) => { const data = item.data; if (item.type === "user_message")
305
+ return `Human: ${String(data.text ?? "")}`; if (item.type === "assistant_message")
306
+ return `Assistant: ${String(data.text ?? "")}`; if (item.type === "tool_call")
307
+ return `Tool call: ${String(data.tool ?? "")} ${JSON.stringify(item.data)}`; if (item.type === "tool_result")
308
+ return `Tool result: ${JSON.stringify(data.result ?? item.data)}`; return `${item.type}: ${JSON.stringify(item.data)}`; }); const facts = this.ledger.readStream(`turn:${turnId}`).filter((event) => event.type === "turn.retry_started").map((event) => `${event.type}: ${JSON.stringify(event.data)}`); return [...items, ...facts].join("\n"); }
309
+ #failTurn(turnId, reason) { const current = this.ledger.turn(turnId); if (!current || current.status !== "in_progress")
310
+ return; this.#clearHandoffState(turnId); this.ledger.finishTurn(turnId, "failed", { message: reason }, this.#now(), "supervisor"); }
311
+ #preemptGoalTurn(goalId, reason) { const turn = this.ledger.turns().find((candidate) => candidate.status === "in_progress" && candidate.bindingKind === "goal" && candidate.goalId === goalId); if (!turn)
312
+ return; this.#clearHandoffState(turn.id); this.ledger.finishTurn(turn.id, "interrupted", { message: reason }, this.#now(), "supervisor"); const handle = this.#handles.get(turn.id); if (handle)
313
+ void this.#terminateHandle(turn.id, handle).finally(() => this.#handles.delete(turn.id)); }
314
+ async #terminateHandle(turnId, handle) { try {
315
+ await handle.terminate();
316
+ }
317
+ catch (error) {
318
+ this.#recordCleanupFailure(turnId, error);
319
+ } }
320
+ #recordCleanupFailure(turnId, error) { this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: "supervisor", type: "runner.cleanup_failed", data: { message: error instanceof Error ? error.message : String(error) }, ignorable: true }); }
321
+ #scheduleTurnRecovery(sourceWake, triggers, turnId, agent) { if (!sourceWake)
322
+ return; const turn = this.ledger.turn(turnId); if (!turn)
323
+ return; const role = this.#role(agent); const goal = turn.bindingKind === "goal" ? this.ledger.goal(turn.goalId) : null; if (turn.bindingKind === "human" || turn.bindingKind === "goal" && (!goal || goal.phase !== "active" || goal.owner !== agent) || turn.bindingKind === "specialist" && role !== turn.specialistRole)
324
+ return; const prior = Math.max(0, ...triggers.map((trigger) => parseRecoveryTrigger(trigger.triggerRef)?.attempt ?? 0)); const next = prior + 1; if (next < this.#retryPolicy.maxAttempts) {
325
+ const delay = this.#retryPolicy.baseDelayMs * 2 ** prior;
326
+ const binding = turn.bindingKind === "goal" ? goalExecutionBinding(agent, turn.goalId) : specialistBinding(agent, turn.specialistRole);
327
+ this.ledger.putSchedule({ id: recoveryScheduleId(turnId, next), ...binding, nextWakeAt: new Date(this.clock.now().getTime() + delay).toISOString(), reason: recoveryRef(turnId), setBy: "supervisor", status: "pending", resolvedAt: null }, "supervisor", sourceWake.id);
328
+ }
329
+ else if (role === "child" && goal?.parentId) {
330
+ const parent = this.ledger.goal(goal.parentId);
331
+ if (parent?.phase === "active")
332
+ this.#enqueueTrigger(parent.owner, childRetryExhaustedRef(turnId), { goalId: parent.id });
333
+ } }
334
+ #closeStreamingItems(turnId) { const open = this.ledger.turnItems(turnId).filter((item) => item.status === "in_progress"); if (open.some((item) => item.type === "tool_call" || item.type === "user_message"))
335
+ throw new Error("Runner returned while an input or tool call was still open"); for (const item of open)
336
+ this.ledger.putTurnItem({ ...item, status: "completed", completedAt: this.#now() }, "supervisor"); }
337
+ #appendTurnItem(turnId, type, data, actor, id = randomUUID(), status = "completed") {
338
+ const now = this.#now();
339
+ const item = { id, turnId, ordinal: this.ledger.turnItems(turnId).length + 1, type, status, data, createdAt: now, completedAt: status === "in_progress" ? null : now };
340
+ this.ledger.putTurnItem(item, actor);
341
+ return item;
342
+ }
343
+ #recordTurnTrace(turnId, leaseToken, type, data, actor = "ceo") {
344
+ if (type === "transcript.completed" || type === "transcript.interrupted")
345
+ return;
346
+ if (type === "transcript.started" && this.ledger.readStream(`turn:${turnId}`).some((event) => event.type === "transcript.started"))
347
+ return;
348
+ this.ledger.appendTurnEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor, type, data }, leaseToken);
349
+ if (type === "message.assistant.completed") {
350
+ const value = data && typeof data === "object" && !Array.isArray(data) ? data : {};
351
+ if (!["ordinary", "handoff"].includes(String(value.completionIntent)) || value.commitState !== (value.completionIntent === "handoff" ? "provisional" : "committed"))
352
+ throw new Error("Runner assistant completion is missing normalized intent/state");
353
+ const text = messageTextContent(value.message?.content);
354
+ if (text)
355
+ this.#appendTurnItem(turnId, "assistant_message", { text }, actor);
356
+ }
357
+ else if (type === "plan.updated")
358
+ this.#appendTurnItem(turnId, "plan", data, actor);
359
+ else if (type === "message.assistant.delta") {
360
+ const input = data;
361
+ const delta = input.delta;
362
+ if (delta?.type === "thinking_start" || delta?.type === "thinking_delta" || delta?.type === "thinking_end") {
363
+ const turn = this.ledger.turn(turnId);
364
+ const id = `${turnId}:attempt:${turn.attempt}:reasoning:${String(input.messageId ?? "message")}`;
365
+ const existing = this.ledger.turnItems(turnId).find((item) => item.id === id);
366
+ const text = `${String(existing?.data?.text ?? "")}${delta.type === "thinking_delta" ? String(delta.delta ?? "") : ""}`;
367
+ if (!existing)
368
+ this.#appendTurnItem(turnId, "reasoning", { text }, actor, id, delta.type === "thinking_end" ? "completed" : "in_progress");
369
+ else
370
+ this.ledger.putTurnItem({ ...existing, data: { text }, status: delta.type === "thinking_end" ? "completed" : "in_progress", completedAt: delta.type === "thinking_end" ? this.#now() : null }, actor);
371
+ }
372
+ }
373
+ else if (type === "tool.called") {
374
+ const input = data;
375
+ const turn = this.ledger.turn(turnId);
376
+ const id = `${turnId}:attempt:${turn.attempt}:tool:${String(input.callId)}`;
377
+ if (this.ledger.turnItems(turnId).some((item) => item.id === id))
378
+ throw new Error("duplicate tool call id in Turn attempt");
379
+ this.#appendTurnItem(turnId, "tool_call", { callId: String(input.callId), tool: String(input.name), arguments: input.arguments ?? null }, actor, id, "in_progress");
380
+ }
381
+ else if (type === "tool.completed") {
382
+ const input = data;
383
+ const turn = this.ledger.turn(turnId);
384
+ const id = `${turnId}:attempt:${turn.attempt}:tool:${String(input.callId)}`;
385
+ const existing = this.ledger.turnItems(turnId).find((item) => item.id === id);
386
+ if (!existing)
387
+ throw new Error("tool result has no matching call in Turn attempt");
388
+ this.ledger.putTurnItem({ ...existing, status: input.isError ? "failed" : "completed", completedAt: this.#now() }, actor);
389
+ this.#appendTurnItem(turnId, "tool_result", { callId: String(input.callId), result: input.result ?? null }, actor);
390
+ }
122
391
  }
123
392
  sendToCeo(body, level = "decision") {
124
- const mail = { id: randomUUID(), to: "ceo", from: "human", level, body, readAt: null };
393
+ const mail = { id: randomUUID(), to: "ceo", from: "human", level, ...humanInboxRoute(), body, readAt: null };
125
394
  this.ledger.putMail(mail, "human");
126
395
  const wake = this.#enqueueTrigger("ceo", `mail:${mail.id}`);
127
396
  if (!wake)
128
397
  throw new Error("CEO wake was not admitted");
129
398
  return { mail, wake };
130
399
  }
131
- delegate(request, actor = "ceo", wakeId) { return this.ledger.commitDelegation(request, actor, wakeId); }
132
- reassignGoal(request, actor = "ceo", wakeId) { return this.ledger.commitReassignment(request, actor, wakeId); }
400
+ delegate(request, actor = "ceo", wakeId) { const candidate = { ...request.childGoal, parentId: request.parentGoalId, phase: "active", revision: 0 }; if (!this.#validGoalOwner(candidate.owner, candidate))
401
+ throw new Error("Child Goal owner must use a Child Agent profile"); return this.ledger.commitDelegation(request, actor, wakeId); }
402
+ async reassignGoal(request, actor = "ceo", wakeId) { const goal = this.#goal(request.goalId); if (!this.#validGoalOwner(request.newOwner, { ...goal, owner: request.newOwner }))
403
+ throw new Error("Child Goal owner must use a Child Agent profile"); const thread = this.ledger.threads().find((candidate) => candidate.agent === goal.owner); const active = thread ? this.ledger.activeTurn(thread.id) : null; if (active?.bindingKind === "goal" && active.goalId === goal.id)
404
+ await this.interruptTurn(active.id); return this.ledger.commitReassignment(request, actor, wakeId); }
133
405
  teamList(now = this.#now()) { return deriveTeam(this.ledger, now); }
134
- updateGoal(id, patch, actor = "human") {
135
- if (patch.objective === undefined && patch.observationMethod === undefined && patch.verificationMethod === undefined && patch.owner === undefined)
136
- throw new Error("goal update requires objective, observation method, verification method, or owner");
406
+ agentRole(agent) { return this.#role(agent); }
407
+ updateGoal(id, patch, actor = "human", change) {
408
+ if (patch.objective === undefined && patch.observationMethod === undefined && patch.verificationMethod === undefined)
409
+ throw new Error("goal update requires objective, observation method, or verification method");
137
410
  const current = this.#goal(id);
411
+ this.#assertGoalMutationAuthority(current, actor);
138
412
  if (patch.objective !== undefined && patch.objective !== current.objective && current.parentId !== null && (patch.observationMethod === undefined || patch.verificationMethod === undefined))
139
413
  throw new Error("child objective revision requires replacement observation and verification methods");
140
414
  const next = {
@@ -144,18 +418,19 @@ export class Supervisor {
144
418
  ...(patch.objective !== undefined && patch.objective !== current.objective && current.parentId === null && patch.verificationMethod === undefined ? { verificationMethod: null } : {}),
145
419
  revision: current.revision + 1,
146
420
  };
147
- this.ledger.putGoal(next, actor);
421
+ this.#preemptGoalTurn(current.id, "Goal definition changed");
422
+ this.ledger.putGoal(next, actor, change?.sourceWakeId, { operation: "revise", reason: change?.reason ?? "Goal definition revised", evidence: change?.evidence ?? [], ...(change?.sourceTurnId ? { sourceTurnId: change.sourceTurnId } : {}), ...(change?.sourceWakeId ? { sourceWakeId: change.sourceWakeId } : {}) });
148
423
  if (next.parentId === null && next.owner === "ceo" && actor === "human")
149
- this.#enqueueTrigger("ceo", `root:${id}:revised:${next.revision}`);
424
+ this.#enqueueTrigger("ceo", `root:${id}:revised:${next.revision}`, { goalId: next.id });
150
425
  return next;
151
426
  }
152
427
  confirmObservationMethod(id, observationMethod) {
153
428
  const current = this.#goal(id);
154
429
  if (current.parentId !== null)
155
430
  throw new Error("human confirmation applies only to a root goal");
156
- return this.updateGoal(id, { observationMethod, verificationMethod: observationMethod }, "human");
431
+ return this.updateGoal(id, { observationMethod, verificationMethod: observationMethod }, "human", { reason: "Human confirmed the Root observation and verification method", evidence: [] });
157
432
  }
158
- reviseChildGoal(id, objective, observationMethod, verificationMethod, actor, reason, evidence, wakeId) {
433
+ reviseChildGoal(id, objective, observationMethod, verificationMethod, actor, reason, evidence, wakeId, sourceTurnId) {
159
434
  const current = this.#goal(id);
160
435
  if (current.parentId === null)
161
436
  throw new Error("CEO cannot revise a root goal");
@@ -165,143 +440,119 @@ export class Supervisor {
165
440
  if (!this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
166
441
  throw new Error(`evidence event does not exist: ${seq}`);
167
442
  this.ledger.appendEvent({ streamId: wakeId ? wakeStream(wakeId) : goalStream(id), ts: this.#now(), actor, type: "goal.revision_requested", data: { goalId: id, fromRevision: current.revision, objective, observationMethod, verificationMethod, reason, evidence } });
168
- return this.updateGoal(id, { objective, observationMethod, verificationMethod }, actor);
443
+ return this.updateGoal(id, { objective, observationMethod, verificationMethod }, actor, { reason, evidence, ...(wakeId ? { sourceWakeId: wakeId } : {}), ...(sourceTurnId ? { sourceTurnId } : {}) });
169
444
  }
170
445
  completeGoal(request, actor = "human", wakeId) {
171
446
  const goal = this.ledger.completeGoal(request, actor, wakeId);
172
- this.#suppressQueuedWake(goal.owner, `goal:${goal.id}:complete`);
173
- if (goal.parentId && actor !== "ceo")
174
- this.#enqueueTrigger("ceo", `goal:${goal.id}:complete:${goal.revision}`);
447
+ this.#preemptGoalTurn(goal.id, "Goal completed");
175
448
  return goal;
176
449
  }
177
- transitionGoal(id, phase, actor = "human") {
450
+ transitionGoal(id, phase, actor = "human", scheduleMotion = true, sourceTurnId) {
178
451
  const current = this.#goal(id);
452
+ this.#assertGoalMutationAuthority(current, actor);
179
453
  if (current.phase === phase)
180
454
  return current;
181
455
  if (phase === "complete")
182
456
  throw new Error("goal completion requires reason and evidence");
183
457
  const next = { ...current, phase, revision: current.revision + 1 };
184
- this.ledger.putGoal(next, actor);
185
- if (phase === "paused")
186
- this.#suppressQueuedWake(next.owner, `goal:${id}:${phase}`);
187
- if (phase === "active")
188
- this.#enqueueTrigger(next.owner, `${next.parentId ? "goal" : "root"}:${id}:resumed:${next.revision}`);
189
- if (next.parentId && actor !== "ceo" && phase === "blocked")
190
- this.#enqueueTrigger("ceo", `goal:${id}:${phase}:${next.revision}`);
458
+ const operation = phase === "paused" ? "pause" : phase === "blocked" ? "block" : "resume";
459
+ this.#preemptGoalTurn(current.id, `Goal ${operation}`);
460
+ this.ledger.putGoal(next, actor, undefined, { operation, reason: `Goal ${operation} requested by ${actor}`, evidence: [], ...(sourceTurnId ? { sourceTurnId } : {}) });
461
+ if (phase === "active" && scheduleMotion)
462
+ this.#enqueueTrigger(next.owner, `${next.parentId ? "goal" : "root"}:${id}:resumed:${next.revision}`, { goalId: next.id });
191
463
  return next;
192
464
  }
193
- planWake(agent, at, reason, setBy = agent) {
194
- const schedule = { id: `schedule:${agent}`, agent, nextWakeAt: at, reason, setBy };
195
- this.ledger.putSchedule(schedule, setBy);
196
- return at <= this.#now() ? this.#enqueueSchedule(schedule) : null;
197
- }
198
- async stopAgentWake(agent) {
199
- const wake = this.ledger.wakes().find((item) => item.agent === agent && ["running", "leased", "queued"].includes(item.status));
200
- if (!wake)
201
- return null;
202
- if (wake.status === "queued") {
203
- this.#suppressQueuedWake(agent, "stopped by human");
204
- return this.#wake(wake.id);
465
+ planWake(agent, at, reason, setBy = agent, target) {
466
+ const parsed = Date.parse(at);
467
+ if (!Number.isFinite(parsed))
468
+ throw new Error("schedule time is invalid");
469
+ const nextWakeAt = new Date(parsed).toISOString();
470
+ const binding = target ?? this.#singleActiveGoalTarget(agent);
471
+ const role = this.#role(agent);
472
+ if (!binding && role !== "verifier" && role !== "audit")
473
+ throw new Error("Goal-owning Agent schedules require an active Goal target");
474
+ if (binding) {
475
+ const goal = this.ledger.goal(binding.goalId);
476
+ if (!goal || goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
477
+ throw new Error("schedule target is not an active Goal owned by this Agent");
205
478
  }
206
- const handle = this.#handles.get(wake.id);
207
- if (handle)
208
- await handle.terminate();
209
- else if (wake.runnerPid)
210
- await this.runner.terminateProcess(wake.runnerPid);
211
- return this.#wake(wake.id);
479
+ const executionTarget = binding ? goalExecutionBinding(agent, binding.goalId) : specialistBinding(agent, role);
480
+ const schedule = { id: `schedule:${agent}${binding ? `:${binding.goalId}` : ""}:${randomUUID()}`, ...executionTarget, nextWakeAt, reason, setBy, status: "pending", resolvedAt: null };
481
+ this.ledger.putSchedule(schedule, setBy);
482
+ return nextWakeAt <= this.#now() ? this.#enqueueSchedule(schedule) : null;
212
483
  }
484
+ async stopAgentWake(agent, goalId) { const role = this.#role(agent); if (goalId) {
485
+ const goal = this.#goal(goalId);
486
+ if (!this.#validGoalOwner(agent, goal))
487
+ throw new Error("Goal stop target is not owned by the Agent");
488
+ } const binding = role === "verifier" || role === "audit" ? null : goalId ? { goalId } : this.#singleActiveGoalTarget(agent); if (role !== "verifier" && role !== "audit" && !binding)
489
+ throw new Error("Goal stop requires an explicit Goal target"); const wake = this.ledger.wakes().find((item) => item.agent === agent && (item.status === "queued" || item.status === "claimed") && (binding ? item.bindingKind === "goal" && item.goalId === binding.goalId : item.bindingKind === "specialist")); if (wake)
490
+ this.ledger.cancelWake(wake.id, this.#now()); const thread = this.ledger.threads().find((item) => item.agent === agent); const turn = thread ? this.ledger.activeTurn(thread.id) : null; if (turn && (binding ? turn.bindingKind === "goal" && turn.goalId === binding.goalId : turn.bindingKind === "specialist"))
491
+ await this.interruptTurn(turn.id); return wake ? this.#wake(wake.id) : null; }
213
492
  async recover() {
214
- this.ledger.recoverDispatchingActions();
215
- for (const expired of this.ledger.expiredWakes(this.#now())) {
216
- if (expired.status === "running" && expired.runnerPid)
217
- await this.runner.terminateProcess(expired.runnerPid);
218
- this.ledger.recoverExpiredWake(expired.id, this.#now());
493
+ for (const wake of this.ledger.wakes().filter((candidate) => candidate.status === "claimed"))
494
+ this.ledger.releaseWake(wake.id, this.#now());
495
+ for (const turn of this.ledger.turns().filter((candidate) => candidate.status === "in_progress" && !this.#handles.has(candidate.id))) {
496
+ if (turn.runnerPid)
497
+ await this.runner.terminateProcess(turn.runnerPid, turn.runnerProfileId);
498
+ this.#failTurn(turn.id, "orphaned Runner ownership recovered after Supervisor restart");
499
+ const thread = this.ledger.thread(turn.threadId);
500
+ if (!thread)
501
+ continue;
502
+ if (turn.bindingKind === "goal") {
503
+ const goal = this.ledger.goal(turn.goalId);
504
+ if (goal?.phase === "active" && goal.owner === thread.agent)
505
+ this.#enqueueTrigger(thread.agent, recoveryRef(turn.id), { goalId: goal.id });
506
+ }
507
+ else if (turn.bindingKind === "specialist" && this.#role(thread.agent) === turn.specialistRole)
508
+ this.#enqueueTrigger(thread.agent, recoveryRef(turn.id));
219
509
  }
220
510
  }
221
511
  async tick() {
222
- const claimed = await this.#claimNextWake();
223
- const wake = claimed?.wake ?? null;
512
+ const wake = await this.#claimNextWake();
224
513
  if (!wake)
225
514
  return null;
226
- const leaseToken = claimed.leaseToken;
227
- let running = wake;
228
- let handle = null;
229
- let renewal;
515
+ let createdTurnId = null;
230
516
  try {
231
- running = this.ledger.markWakeRunning(wake.id, this.#now(), leaseToken);
232
- const turn = this.#turnContext(running);
233
- this.ledger.appendRunnerEvent({ streamId: wakeStream(running.id), ts: this.#now(), actor: running.agent, type: "run.admitted", data: turn }, leaseToken);
234
- const workRecordRevisionAtStart = turn.goalBinding ? this.ledger.workRecord(turn.goalBinding.goalId)?.recordRevision ?? -1 : -1;
235
- const context = this.#loadContext(running, turn);
236
- handle = this.runner.prepare({
237
- wake: running,
238
- turn,
239
- context,
240
- now: () => this.#now(),
241
- emit: (trace) => this.ledger.appendRunnerEvent({ streamId: wakeStream(running.id), ts: this.#now(), actor: running.agent, type: trace.type, data: trace.data }, leaseToken),
242
- rpc: (method, params) => this.#agentRpc(running, turn, leaseToken, method, params),
243
- });
244
- this.#handles.set(running.id, handle);
245
- if (handle.pid)
246
- running = this.ledger.attachWakeProcess(running.id, leaseToken, handle.pid, this.#now());
247
- renewal = setInterval(() => {
248
- try {
249
- const now = this.clock.now();
250
- this.ledger.renewWakeLease(running.id, leaseToken, new Date(now.getTime() + this.#leaseMs).toISOString(), now.toISOString());
517
+ await this.#awaitAgentExecution(wake.agent);
518
+ const currentWake = this.#wake(wake.id);
519
+ if (currentWake.status !== "claimed")
520
+ return currentWake;
521
+ if (currentWake.bindingKind === "goal") {
522
+ const goal = this.ledger.goal(currentWake.goalId);
523
+ if (!goal || goal.owner !== currentWake.agent || goal.phase !== "active" || !this.#validGoalOwner(currentWake.agent, goal)) {
524
+ this.ledger.cancelWake(currentWake.id, this.#now());
525
+ return this.#wake(currentWake.id);
251
526
  }
252
- catch {
253
- void handle?.terminate();
254
- }
255
- }, Math.max(25, Math.floor(this.#leaseMs / 3)));
256
- renewal.unref();
257
- handle.begin();
258
- const result = await handle.result;
259
- clearInterval(renewal);
260
- renewal = undefined;
261
- await handle.terminate();
262
- this.#handles.delete(running.id);
263
- if (result.outcome === "abnormal") {
264
- await this.#markAbnormal(running, result.reason);
265
- return this.#wake(running.id);
266
- }
267
- if (result.outcome === "response") {
268
- if (turn.goalBinding)
269
- throw new Error("a Goal-bound Turn must finish with a handoff");
270
- const mailId = interactionMailId(running);
271
- if (!mailId)
272
- throw new Error("an ordinary response requires an interaction mail");
273
- this.ledger.commitInteraction({ agent: running.agent, wakeId: running.id, mailId, ts: this.#now(), response: result.response });
274
- this.ledger.finishWake(running.id, "done", this.#now());
275
- return this.#wake(running.id);
276
- }
277
- if (turn.goalBinding)
278
- this.#validateGoalTurnRecord(running, turn, workRecordRevisionAtStart);
279
- const output = turn.goalBinding ? this.#goalWakeOutput(result.output, turn.goalBinding) : result.output;
280
- this.#validateCeoHandoff(running, output);
281
- const outgoingMail = output.mail.map((draft) => ({ id: randomUUID(), to: draft.to, from: running.agent, level: draft.level, body: draft.body, readAt: null }));
282
- const schedule = output.nextWakeAt
283
- ? { id: `schedule:${running.agent}`, agent: running.agent, nextWakeAt: output.nextWakeAt, reason: "handoff.next_steps", setBy: running.agent }
284
- : null;
285
- const handoffEvent = this.ledger.commitHandoff({ agent: running.agent, wakeId: running.id, ts: this.#now(), output, outgoingMail, schedule });
286
- this.ledger.finishWake(running.id, "done", this.#now());
287
- if (this.#role(running.agent) !== "ceo" && handoffTriggersCeo(output.handoff) && this.#hasActiveRoot()) {
288
- this.#enqueueTrigger("ceo", `child-handoff:${handoffEvent.seq}`);
289
527
  }
290
- if (this.#verifyMetricsAfterWake) {
291
- for (const goal of this.ledger.goalsForOwner(running.agent))
292
- if (this.#metricCollectors.has(goal.id))
293
- await this.collectMetricNow(goal.id);
528
+ const wakeTriggers = this.ledger.wakeTriggers(currentWake.id).filter((trigger) => trigger.status === "pending");
529
+ const turnContext = this.#turnContext(currentWake, wakeTriggers);
530
+ if (!this.#validExecution(currentWake.agent, turnContext)) {
531
+ this.ledger.cancelWake(currentWake.id, this.#now());
532
+ return this.#wake(currentWake.id);
294
533
  }
295
- return this.#wake(running.id);
534
+ const thread = this.threadFor(currentWake.agent);
535
+ const now = this.#now();
536
+ const leaseToken = randomUUID();
537
+ const turnBinding = currentWake.bindingKind === "goal" ? goalTurnBinding(currentWake.goalId, turnContext.goalBinding.goalRevision) : currentWake.bindingKind === "human" ? humanTurnBinding() : specialistTurnBinding(currentWake.specialistRole);
538
+ const execution = { id: randomUUID(), threadId: thread.id, source: turnContext.source.kind, ...turnBinding, status: "in_progress", attempt: 1, error: null, startedAt: now, endedAt: null, leaseUntil: new Date(this.clock.now().getTime() + this.#leaseMs).toISOString(), leaseToken, runnerPid: null, runnerProfileId: this.#runnerProfileId(currentWake.agent) };
539
+ this.ledger.putThread({ ...thread, updatedAt: this.#now() }, "supervisor");
540
+ this.ledger.startTurnFromWake(currentWake.id, execution, now);
541
+ createdTurnId = execution.id;
542
+ const deliveredMail = this.#contextMail(currentWake);
543
+ const deliveredMailIds = deliveredMail.map((mail) => mail.id);
544
+ this.ledger.appendEvent({ streamId: `turn:${execution.id}`, ts: now, actor: "supervisor", type: "run.admitted", data: { ...turnContext, wakeTriggers }, ignorable: true });
545
+ const revision = turnContext.goalBinding ? this.ledger.workRecord(turnContext.goalBinding.goalId)?.recordRevision ?? -1 : -1;
546
+ await this.#trackExecution(execution, currentWake.agent, turnContext, () => this.#goalContext(currentWake, turnContext, execution.id, deliveredMail, wakeTriggers), currentWake, deliveredMailIds, revision, wakeTriggers);
547
+ return this.#wake(wake.id);
296
548
  }
297
549
  catch (error) {
298
- if (renewal)
299
- clearInterval(renewal);
300
- if (handle)
301
- await handle.terminate();
302
- this.#handles.delete(running.id);
303
- await this.#markAbnormal(running, error instanceof Error ? error.message : String(error));
304
- return this.#wake(running.id);
550
+ if (createdTurnId && this.ledger.turn(createdTurnId)?.status === "in_progress")
551
+ this.#failTurn(createdTurnId, error instanceof Error ? error.message : String(error));
552
+ const current = this.ledger.wake(wake.id);
553
+ if (current?.status === "claimed")
554
+ this.ledger.releaseWake(wake.id, this.#now());
555
+ return this.#wake(wake.id);
305
556
  }
306
557
  }
307
558
  async #claimNextWake() {
@@ -312,15 +563,24 @@ export class Supervisor {
312
563
  try {
313
564
  for (const schedule of this.ledger.dueSchedules(this.#now()))
314
565
  this.#enqueueSchedule(schedule);
315
- await this.#collectMetrics();
316
- this.#scheduleMetricAlerts();
317
- this.#checkSystemSilence();
318
- for (const mail of this.ledger.triggeringMail())
319
- this.#enqueueTrigger(mail.to, `mail:${mail.id}`);
320
- const now = this.clock.now();
321
- const leaseToken = randomUUID();
322
- const wake = this.ledger.claimNextWake(now.toISOString(), new Date(now.getTime() + this.#leaseMs).toISOString(), leaseToken);
323
- return wake ? { wake, leaseToken } : null;
566
+ for (const mail of this.ledger.triggeringMail()) {
567
+ const thread = this.ledger.threads().find((candidate) => candidate.agent === mail.to);
568
+ if (thread && this.ledger.activeTurn(thread.id))
569
+ continue;
570
+ const goal = mail.routeKind === "goal" ? this.ledger.goal(mail.goalId) : null;
571
+ const target = goal && goal.owner === mail.to && goal.phase === "active" && this.#validGoalOwner(mail.to, goal) ? { goalId: goal.id } : undefined;
572
+ const humanInteraction = mail.routeKind === "human_inbox" && mail.to === "ceo" && mail.from === "human";
573
+ const specialistInteraction = mail.routeKind === "specialist_inbox" && this.#role(mail.to) === mail.specialistRole;
574
+ if (!target && !humanInteraction && !specialistInteraction)
575
+ continue;
576
+ const base = `mail:${mail.id}`;
577
+ const related = this.ledger.wakeTriggersForAgent(mail.to).map((trigger) => ({ trigger, attempt: mailDeliveryAttempt(trigger.triggerRef, base) })).filter((entry) => entry.attempt !== null);
578
+ const pending = related.filter((entry) => entry.trigger.status === "pending").sort((a, b) => b.attempt - a.attempt)[0];
579
+ const next = related.reduce((highest, entry) => Math.max(highest, entry.attempt), -1) + 1;
580
+ const trigger = pending?.trigger.triggerRef ?? (next === 0 ? base : `${base}@redelivery:${next}`);
581
+ this.#enqueueTrigger(mail.to, trigger, target);
582
+ }
583
+ return this.ledger.claimNextWake(this.#now());
324
584
  }
325
585
  finally {
326
586
  release();
@@ -336,129 +596,55 @@ export class Supervisor {
336
596
  return completed;
337
597
  }
338
598
  }
339
- async submitAction(action, connectorName, wakeId) {
340
- const connector = this.#connectors.get(connectorName);
341
- const capability = connector ? capabilityFor(connector.manifest, action.kind) : null;
342
- const gated = !connector || !capability || capability.risk !== "reversible"
343
- || (!connector.manifest.dryRun && !this.#allowExternalActions);
344
- if (gated && action.agent !== "ceo")
345
- this.#assertAgentGoalsCurrent(action.agent);
346
- const requested = { ...action, connector: connectorName, gated, status: "requested", reconciledAt: null, externalRef: null };
347
- this.ledger.requestAction(requested, action.agent, wakeId);
348
- if (gated || !connector)
349
- return this.#action(action.id);
350
- this.ledger.approveAction(action.id, "supervisor", "declared reversible dry-run capability", action.evidence);
351
- return this.#dispatch(action.id);
352
- }
353
- async approveAction(id, approver, reason, evidence) {
354
- if (!this.#approvers.has(approver))
355
- throw new Error("actor is not authorized to approve actions");
356
- this.ledger.approveAction(id, approver, reason, evidence);
357
- return this.#dispatch(id);
358
- }
359
- rejectAction(id, approver, reason, evidence) {
360
- if (!this.#approvers.has(approver))
361
- throw new Error("actor is not authorized to reject actions");
362
- return this.ledger.rejectAction(id, approver, reason, evidence);
363
- }
364
- putAuditAdvice(id, advice, wakeId) {
365
- const role = this.#profiles.get(advice.by)?.role;
366
- if (!this.#auditWriters.has(advice.by) && role !== "verifier" && role !== "audit")
367
- throw new Error("actor is not authorized to write audit advice");
368
- return this.ledger.putAuditAdvice(id, advice, wakeId);
369
- }
370
- ackAuditAdvice(id, agent) { return this.ledger.ackAuditAdvice(id, agent); }
371
- recordMetric(sample) {
372
- const goal = this.ledger.goal(sample.goalId);
373
- if (!goal)
374
- throw new Error(`metric goal not found: ${sample.goalId}`);
375
- const contract = this.#metricContracts.get(goal.id);
376
- if (!contract)
377
- throw new Error(`metric contract is not registered: ${goal.id}`);
378
- if (contract.source !== sample.source)
379
- throw new Error("metric source does not match registered contract");
380
- this.ledger.appendEvent({ streamId: `metric:${goal.id}`, ts: this.#now(), actor: "supervisor", type: "metric.sampled", data: sample });
381
- const evaluation = evaluateMetric(contract, this.ledger.metricSamples(goal.id), this.#now());
382
- this.ledger.appendEvent({ streamId: `metric:${goal.id}`, ts: this.#now(), actor: "supervisor", type: "metric.evaluated", data: evaluation });
383
- if (evaluation.shouldWakeOwner)
384
- this.#enqueueTrigger(goal.owner, `metric:${goal.id}:${sample.observedAt}`);
385
- return evaluation;
386
- }
387
- async collectMetricNow(goalId) {
388
- const registration = this.#metricCollectors.get(goalId);
389
- if (!registration)
390
- throw new Error(`metric collector is not registered: ${goalId}`);
391
- registration.nextAt = this.clock.now().getTime() + registration.intervalMs;
392
- const sample = await runJsonProcess(registration.spec, { goalId });
393
- return this.recordMetric({ ...sample, goalId });
394
- }
395
- async reconcileAction(id) {
396
- const action = this.#action(id);
397
- if (action.status !== "unknown")
398
- throw new Error("only unknown actions may be reconciled");
399
- const connector = this.#requiredConnector(action.connector);
400
- const capability = capabilityFor(connector.manifest, action.kind);
401
- if (!capability || capability.query === "none")
402
- throw new Error("connector cannot reconcile this action");
403
- const result = await runConnector(connector, "query", action);
404
- if (result.status === "pending")
405
- return action;
406
- return this.ledger.transitionAction(id, result.status, { reconciledAt: this.#now(), ...(result.externalRef ? { externalRef: result.externalRef } : {}) });
407
- }
408
- async retryUnknownAction(id) {
409
- const action = this.#action(id);
410
- if (action.status !== "unknown")
411
- throw new Error("only unknown actions may be retried");
412
- const connector = this.#requiredConnector(action.connector);
413
- const capability = capabilityFor(connector.manifest, action.kind);
414
- if (!capability?.nativeIdempotency || !capability.automaticRetry)
415
- throw new Error("connector manifest forbids automatic retry");
416
- return this.#dispatch(id);
417
- }
418
- async #dispatch(id) {
419
- const current = this.#action(id);
420
- const connector = this.#requiredConnector(current.connector);
421
- const dispatching = this.ledger.transitionAction(id, "dispatching");
422
- const result = await runConnector(connector, "dispatch", dispatching);
423
- return this.ledger.transitionAction(id, result.status, result.externalRef ? { externalRef: result.externalRef } : {});
424
- }
425
- async #markAbnormal(wake, reason) {
426
- const current = this.#wake(wake.id);
427
- if (current.runnerPid)
428
- await this.runner.terminateProcess(current.runnerPid);
429
- this.ledger.appendEvent({ streamId: wakeStream(current.id), ts: this.#now(), actor: current.agent, type: "wake.abnormal_reason", data: { reason } });
430
- const repairs = interruptedSessionEvents(this.ledger.eventsForWake(current.id), this.#now(), "supervisor");
431
- if (repairs.length)
432
- this.ledger.appendEvents(repairs);
433
- if (["leased", "running", "queued"].includes(current.status)) {
434
- this.ledger.finishWake(current.id, "abnormal", this.#now());
435
- if (current.attempt < this.#retryPolicy.maxAttempts) {
436
- const delay = this.#retryPolicy.baseDelayMs * 2 ** Math.max(0, current.attempt - 1);
437
- const schedule = { id: `retry:${current.id}`, agent: current.agent, nextWakeAt: new Date(this.clock.now().getTime() + delay).toISOString(), reason: `recovery:${current.id}`, setBy: "supervisor" };
438
- this.ledger.putSchedule(schedule, "supervisor", current.id);
439
- }
440
- else if (current.agent !== "ceo" && this.#hasActiveRoot()) {
441
- this.#enqueueTrigger("ceo", `child-retry-exhausted:${current.id}`);
599
+ #enqueueSchedule(schedule) {
600
+ if (schedule.status !== "pending")
601
+ return null;
602
+ if (schedule.bindingKind === "goal") {
603
+ const goal = this.ledger.goal(schedule.goalId);
604
+ if (!goal || goal.owner !== schedule.agent || goal.phase !== "active" || !this.#validGoalOwner(schedule.agent, goal)) {
605
+ this.ledger.supersedeSchedule(schedule.id, this.#now());
606
+ return null;
442
607
  }
443
608
  }
609
+ else if (!["verifier", "audit"].includes(this.#role(schedule.agent))) {
610
+ this.ledger.cancelSchedule(schedule.id, this.#now());
611
+ return null;
612
+ }
613
+ const wake = { id: `wake:${schedule.id}`, ...(schedule.bindingKind === "goal" ? goalExecutionBinding(schedule.agent, schedule.goalId) : specialistBinding(schedule.agent, schedule.specialistRole)), triggerRef: `${schedule.id}@${schedule.nextWakeAt}`, status: "queued", attempt: 0, enqueuedSeq: 0, claimedAt: null, consumedAt: null, turnId: null };
614
+ return this.ledger.consumeSchedule(schedule.id, wake, this.#now()).wake;
444
615
  }
445
- #enqueueSchedule(schedule) {
446
- return this.#enqueueTrigger(schedule.agent, `${schedule.id}@${schedule.nextWakeAt}`);
447
- }
448
- #enqueueTrigger(agent, triggerRef) {
616
+ #enqueueTrigger(agent, triggerRef, target) {
617
+ let binding;
618
+ if (target) {
619
+ const goal = this.ledger.goal(target.goalId);
620
+ if (!goal || goal.owner !== agent || goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
621
+ return null;
622
+ binding = goalExecutionBinding(agent, target.goalId);
623
+ }
624
+ else {
625
+ const mailId = mailIdFromTriggerRef(triggerRef);
626
+ const mail = mailId ? this.ledger.mailbox().find((candidate) => candidate.id === mailId) : null;
627
+ const role = this.#role(agent);
628
+ if (agent === "ceo" && role === "ceo" && mail?.routeKind === "human_inbox" && mail.from === "human")
629
+ binding = humanBinding();
630
+ else if (role === "verifier" || role === "audit")
631
+ binding = specialistBinding(agent, role);
632
+ else
633
+ return null;
634
+ }
635
+ const sameBinding = (wake) => wake.bindingKind === binding.bindingKind && wake.goalId === binding.goalId && wake.specialistRole === binding.specialistRole;
449
636
  const exact = this.ledger.wakeByTrigger(agent, triggerRef);
450
- if (exact)
637
+ if (exact) {
638
+ if (!sameBinding(exact))
639
+ throw new Error("Wake trigger was reused with a different execution binding");
451
640
  return exact;
452
- const ownsLiveGoal = this.ledger.goalsForOwner(agent).some((goal) => goal.phase === "active" || goal.phase === "blocked");
453
- const ceoInterrupt = agent === "ceo" && (triggerRef.startsWith("interaction:") || triggerRef.startsWith("mail:") || triggerRef.startsWith("child-"));
454
- if (!ownsLiveGoal && !ceoInterrupt)
455
- return null;
456
- const queued = triggerRef.startsWith("interaction:") ? null : this.ledger.queuedWakeForAgent(agent);
457
- if (queued) {
458
- this.ledger.appendEvent({ streamId: wakeStream(queued.id), ts: this.#now(), actor: "supervisor", type: "wake.trigger_coalesced", data: { wakeId: queued.id, triggerRef } });
459
- return queued;
460
641
  }
461
- const wake = { id: randomUUID(), agent, triggerRef, status: "queued", leaseUntil: null, attempt: 0, startedAt: null, endedAt: null, enqueuedSeq: 0, leaseToken: null, runnerPid: null };
642
+ const pending = this.ledger.wakes().find((wake) => wake.agent === agent && wake.status === "claimed" && sameBinding(wake)) ?? this.ledger.wakes().find((wake) => wake.agent === agent && wake.status === "queued" && sameBinding(wake));
643
+ if (pending) {
644
+ this.ledger.addWakeTrigger(pending.id, triggerRef, "supervisor");
645
+ return pending;
646
+ }
647
+ const wake = { id: randomUUID(), ...binding, triggerRef, status: "queued", attempt: 0, enqueuedSeq: 0, claimedAt: null, consumedAt: null, turnId: null };
462
648
  const result = this.ledger.enqueueWake(wake, "supervisor");
463
649
  if (result.created)
464
650
  return this.#wake(wake.id);
@@ -467,303 +653,270 @@ export class Supervisor {
467
653
  throw new Error("deduplicated wake is missing");
468
654
  return existing;
469
655
  }
470
- #suppressQueuedWake(agent, reason) {
471
- const wake = this.ledger.queuedWakeForAgent(agent);
472
- if (!wake)
473
- return;
474
- this.ledger.appendEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: "supervisor", type: "wake.suppressed", data: { reason } });
475
- this.ledger.finishWake(wake.id, "abnormal", this.#now());
476
- }
477
656
  #hasActiveRoot() { return this.ledger.goals().some((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase === "active"); }
478
- #role(agent) { return this.#profiles.get(agent)?.role ?? "child"; }
479
- #validateCeoHandoff(wake, output) {
480
- if (this.#role(wake.agent) !== "ceo")
481
- return;
482
- const idle = this.teamList().filter((member) => member.agent !== "ceo" && member.status === "idle_unplanned");
483
- const missingObservationGoalIds = this.ledger.goals().filter((goal) => goal.parentId !== null && goal.phase !== "complete" && goal.observationMethod === null).map((goal) => goal.id);
484
- const activeRoot = this.#hasActiveRoot();
485
- const hasChildMotion = this.teamList().some((member) => member.agent !== "ceo" && !["idle_unplanned", "retired"].includes(member.status));
486
- const hasReview = Boolean(output.nextWakeAt);
487
- const hasBlocker = handoffBlocked(output.handoff);
488
- const asksHuman = output.mail.some((mail) => mail.to === "human" && (mail.level === "decision" || mail.level === "emergency"))
489
- || this.ledger.unreadMail("human").some((mail) => mail.from === "ceo" && (mail.level === "decision" || mail.level === "emergency"));
490
- if (idle.length === 0 && missingObservationGoalIds.length === 0 && (!activeRoot || hasChildMotion || hasReview || hasBlocker || asksHuman))
491
- return;
492
- const violation = {
493
- idleAgents: idle.map((member) => member.agent),
494
- missingObservationGoalIds,
495
- reason: missingObservationGoalIds.length ? "active child goal has no observation method" : idle.length ? "active child goal has no liveness route" : "active root has no motion, review, wait, blocker, or human request",
496
- };
497
- this.ledger.appendEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: "supervisor", type: "ceo.motion_invalid", data: violation });
498
- throw new Error(`CEO motion invalid: ${violation.reason}${violation.idleAgents.length ? ` (${violation.idleAgents.join(", ")})` : ""}`);
499
- }
500
- #validateGoalTurnRecord(wake, turn, revisionAtStart) {
501
- const binding = turn.goalBinding;
502
- const goal = this.#goal(binding.goalId);
503
- if (goal.revision !== binding.goalRevision)
504
- throw new Error("Goal revision changed during the Turn");
505
- const record = this.ledger.workRecord(goal.id);
506
- if (!record || record.recordRevision <= revisionAtStart || record.updatedInTurn !== wake.id || record.goalRevision !== goal.revision)
507
- throw new Error("Goal-bound Turn must update its Work Record before handoff");
508
- }
509
- #goalWakeOutput(output, binding) {
657
+ #activeRoot() { return this.ledger.goals().find((goal) => goal.parentId === null && goal.owner === "ceo" && goal.phase === "active") ?? null; }
658
+ #singleActiveGoalTarget(agent) { const goals = this.ledger.goalsForOwner(agent).filter((goal) => goal.phase === "active" && this.#validGoalOwner(agent, goal)); if (goals.length > 1)
659
+ throw new Error("Wake requires an explicit Goal target when an Agent owns multiple active Goals"); return goals.length === 1 ? { goalId: goals[0].id } : undefined; }
660
+ #knownAgent(agent) { return this.#profiles.has(agent) || this.ledger.threads().some((thread) => thread.agent === agent) || this.ledger.goals().some((goal) => goal.owner === agent); }
661
+ #role(agent) { return this.#profiles.get(agent)?.role ?? (agent === "ceo" ? "ceo" : "child"); }
662
+ #validGoalOwner(agent, goal) { const role = this.#role(agent); return goal.owner === agent && (role === "ceo" ? agent === "ceo" && goal.parentId === null : role === "child" && goal.parentId !== null); }
663
+ #validExecution(agent, turn) { const role = this.#role(agent); if (turn.goalBinding) {
664
+ const goal = this.ledger.goal(turn.goalBinding.goalId);
665
+ return Boolean(goal && this.#validGoalOwner(agent, goal));
666
+ } if (turn.source.kind === "human")
667
+ return agent === "ceo" && role === "ceo"; return role === "verifier" || role === "audit"; }
668
+ #commitGoalTurn(turnId, agent, turn, raw, sourceWakeId, mailIds, revisionAtStart) { const binding = turn.goalBinding; const goal = this.#goal(binding.goalId); if (goal.revision !== binding.goalRevision)
669
+ throw new Error("Goal revision changed during the Turn"); const record = this.ledger.workRecord(goal.id); if (!record || record.recordRevision <= revisionAtStart || record.updatedInTurn !== turnId || record.goalRevision !== goal.revision)
670
+ throw new Error("Goal-bound Turn must update its Work Record before handoff"); assertTurnOutput(raw); const validation = this.#handoffValidations.get(raw.validationToken); const execution = this.ledger.turn(turnId); if (!validation || !execution || validation.turnId !== turnId || validation.attemptId !== raw.validationAttemptId || this.#handoffAttemptSeq.get(turnId) !== raw.validationAttemptId || validation.agent !== agent || validation.attempt !== execution.attempt || validation.leaseToken !== execution.leaseToken || validation.goalId !== goal.id || validation.goalRevision !== goal.revision || validation.handoff.outcome !== raw.handoff.outcome || !isSameNumbers(validation.handoff.evidence, raw.handoff.evidence))
671
+ throw new Error("Handoff validation token is stale or does not match this Turn"); const responseItem = [...this.ledger.turnItems(turnId)].reverse().find((item) => item.type === "assistant_message" && item.status === "completed" && item.data.text === validation.message); if (!responseItem)
672
+ throw new Error("accepted Handoff response was not recorded in the Turn"); const output = this.#goalTurnOutput(raw, binding); assertHandoff(output.handoff); const now = this.#now(); const item = { id: randomUUID(), turnId, ordinal: this.ledger.turnItems(turnId).length + 1, type: "handoff", status: "completed", data: output.handoff, createdAt: now, completedAt: now }; this.ledger.commitHandoff({ agent, turnId, sourceWakeId, mailIds, ts: now, output, responseItemId: responseItem.id, item }); this.#clearHandoffState(turnId); }
673
+ #goalTurnOutput(output, binding) {
510
674
  const record = this.ledger.workRecord(binding.goalId);
511
675
  if (!record)
512
676
  throw new Error("Goal Work Record is missing");
513
- const legacy = "goalId" in output.handoff ? null : output.handoff;
514
677
  const handoff = {
515
678
  goalId: binding.goalId,
516
679
  goalRevision: binding.goalRevision,
517
680
  recordRevision: record.recordRevision,
518
- outcome: "goalId" in output.handoff ? output.handoff.outcome : legacy?.blocker ? "blocked" : legacy?.material ? "completion_proposed" : "progress",
519
- evidence: "goalId" in output.handoff && output.handoff.evidence.length ? output.handoff.evidence : record.evidence,
681
+ outcome: output.handoff.outcome,
682
+ evidence: output.handoff.evidence,
520
683
  };
521
- return { ...output, handoff };
684
+ return { handoff };
522
685
  }
523
- #turnContext(wake) {
524
- if (interactionMailId(wake))
686
+ #turnContext(wake, triggers = this.ledger.wakeTriggers(wake.id).filter((trigger) => trigger.status === "pending")) {
687
+ if (wake.bindingKind === "human")
525
688
  return { source: { kind: "human" } };
526
- const goals = this.ledger.goalsForOwner(wake.agent).filter((goal) => goal.phase === "active");
527
- const goal = goals.find((candidate) => candidate.parentId === null) ?? goals[0];
528
- const humanMail = wake.triggerRef.startsWith("mail:") && this.ledger.mailbox().some((mail) => mail.id === wake.triggerRef.slice(5) && mail.from === "human");
529
- return { source: humanMail ? { kind: "human" } : { kind: "goal_driver", round: goal ? (this.ledger.workRecord(goal.id)?.recordRevision ?? 0) + 1 : 1 }, ...(goal ? { goalBinding: { goalId: goal.id, goalRevision: goal.revision } } : {}) };
689
+ if (wake.bindingKind === "specialist")
690
+ return { source: { kind: "system", reason: triggers.map((trigger) => trigger.triggerRef).join(", ") } };
691
+ const goal = this.#goal(wake.goalId);
692
+ return { source: { kind: "goal", round: (this.ledger.workRecord(goal.id)?.recordRevision ?? 0) + 1 }, goalBinding: { goalId: goal.id, goalRevision: goal.revision } };
530
693
  }
531
- #loadContext(wake, turn) {
694
+ #loadContext(wake, turn, mail, wakeTriggers) {
532
695
  const profile = this.#profiles.get(wake.agent) ?? { agent: wake.agent, role: "child" };
533
696
  const role = profile.role;
534
697
  const capabilities = profile.capabilities ?? defaultCapabilities(role);
535
698
  const runnerProfile = this.#runnerProfiles.get(profile.runnerProfile ?? "default");
536
- if (!turn.goalBinding && interactionMailId(wake)) {
537
- const mail = this.ledger.mailbox().find((candidate) => candidate.id === interactionMailId(wake));
538
- const body = mail?.body && typeof mail.body === "object" && !Array.isArray(mail.body) ? mail.body : {};
539
- const message = typeof body.message === "string" ? body.message : "";
540
- const mailEvent = this.ledger.eventsSince(0, ["mail.put"]).findLast((event) => event.data.snapshot?.id === mail?.id);
541
- const recent = this.ledger.eventsSince(0, ["interaction.completed"]).filter((event) => event.actor === wake.agent).slice(-8).map((event) => {
542
- const data = event.data;
543
- const prior = this.ledger.mailbox().find((candidate) => candidate.id === data.mailId);
544
- const priorBody = prior?.body && typeof prior.body === "object" && !Array.isArray(prior.body) ? prior.body : {};
545
- return `Human: ${typeof priorBody.message === "string" ? priorBody.message : ""}\nAssistant: ${data.response?.content ?? ""}`;
546
- });
547
- return {
548
- text: [...(recent.length ? [`# Recent conversation\n\n${recent.join("\n\n")}`] : []), `# Human message\n\n${message}`].join("\n\n"),
549
- sourceSeqs: mailEvent ? [mailEvent.seq] : [],
550
- capabilities,
551
- systemPrompt: profile.systemPrompt ?? "You are Goah's primary Agent. Respond naturally to the Human, use tools when useful, and keep the final answer concise. Do not create or operate a Goal unless the Human expresses durable Goal intent.",
552
- ...(runnerProfile ? { runnerProfile } : {}),
553
- };
554
- }
555
699
  const goals = role === "ceo" ? this.ledger.goals() : this.ledger.goalsForOwner(wake.agent);
556
- const mail = this.ledger.unreadMail(wake.agent);
557
- const handoff = this.ledger.lastEvent(wake.agent, "handoff.recorded");
558
- const recoveryId = wake.triggerRef.startsWith("recovery:")
559
- ? wake.triggerRef.slice("recovery:".length)
560
- : wake.triggerRef.startsWith("retry:") ? wake.triggerRef.slice("retry:".length).split("@")[0] : null;
561
- const recoveryEvents = recoveryId ? selectRecoveryEvents(this.ledger.eventsForWake(recoveryId)) : [];
700
+ const handoff = turn.goalBinding ? this.ledger.lastGoalHandoff(turn.goalBinding.goalId) : null;
701
+ const recoveryIds = [...new Set(wakeTriggers.flatMap((trigger) => { const parsed = parseRecoveryTrigger(trigger.triggerRef); const exhausted = childRetryExhaustedTurnId(trigger.triggerRef); return parsed ? [parsed.turnId] : exhausted ? [exhausted] : []; }))];
702
+ const recoveryEvents = recoveryIds.flatMap((recoveryId) => selectRecoveryEvents(this.ledger.turn(recoveryId) ? this.ledger.readStream(`turn:${recoveryId}`) : this.ledger.eventsForWake(recoveryId)));
562
703
  const teamHandoffs = role === "ceo"
563
704
  ? [...this.ledger.eventsSince(0, ["handoff.recorded"])].reverse().filter((event, index, all) => all.findIndex((candidate) => candidate.actor === event.actor) === index)
564
705
  : [];
565
- const actions = this.ledger.actions().filter((action) => action.agent === wake.agent && (action.status === "unknown" || Boolean(action.auditAdvice && !action.adviceAcked)));
566
- const revisionWarnings = goals.flatMap((goal) => this.#goalRevisionWarning(goal));
567
706
  const workingMemory = selectWorkingMemory(this.ledger.readStream(memoryStream(wake.agent)), this.#memoryTailChars);
568
- const active = composeActiveContext({ role, capabilities, systemPrompt: profile.systemPrompt ?? defaultRolePrompt(role), wake, goals, mail, actions, lastHandoff: handoff, teamHandoffs, team: role === "ceo" ? this.teamList() : [], revisionWarnings, recoveryEvents, workingMemory });
707
+ const active = composeActiveContext({ role, capabilities, systemPrompt: profile.systemPrompt ?? defaultTurnPrompt(role, wake.agent, turn), wake, wakeTriggers, goals, mail, lastHandoff: handoff, teamHandoffs, team: role === "ceo" ? this.teamList() : [], recoveryEvents, workingMemory });
569
708
  const records = this.ledger.workRecords();
570
709
  const currentRecord = turn.goalBinding ? this.ledger.workRecord(turn.goalBinding.goalId) : null;
571
710
  const currentGoal = turn.goalBinding ? this.ledger.goal(turn.goalBinding.goalId) : null;
572
711
  const parentRecord = currentGoal?.parentId ? this.ledger.workRecord(currentGoal.parentId) : null;
573
712
  const recordIndex = records.map((record) => `- /goals/${record.goalId}.md · r${record.recordRevision} · ${this.ledger.goal(record.goalId)?.owner ?? "unknown"} · ${this.ledger.goal(record.goalId)?.phase ?? "unknown"}`);
574
713
  const workText = [`# Shared Work Record Index\n\n${recordIndex.join("\n")}`, ...(currentRecord ? [`# Your Work Record\n\n${currentRecord.content}`] : []), ...(parentRecord ? [`# Parent Work Record\n\n${parentRecord.content}`] : [])].join("\n\n");
575
- return { ...active, text: `${active.text}\n\n${workText}`, sourceSeqs: [...new Set([...active.sourceSeqs, ...records.map((record) => record.lastEventSeq)])].sort((a, b) => a - b), workRecord: currentRecord, sharedWorkRecords: records, ...(runnerProfile ? { runnerProfile } : {}) };
714
+ return { ...active, text: `${active.text}\n\n${workText}`, sourceSeqs: [...new Set([...active.sourceSeqs, ...this.#mailSourceSeqs(mail), ...records.map((record) => record.lastEventSeq)])].sort((a, b) => a - b), workRecord: currentRecord, sharedWorkRecords: records, ...(runnerProfile ? { runnerProfile } : {}) };
576
715
  }
577
- #requiredConnector(name) { const value = this.#connectors.get(name); if (!value)
578
- throw new Error(`connector not registered: ${name}`); return value; }
579
716
  #wake(id) { const value = this.ledger.wake(id); if (!value)
580
717
  throw new Error(`wake not found: ${id}`); return value; }
581
- #action(id) { const value = this.ledger.action(id); if (!value)
582
- throw new Error(`action not found: ${id}`); return value; }
583
718
  #goal(id) { const value = this.ledger.goal(id); if (!value)
584
719
  throw new Error(`goal not found: ${id}`); return value; }
585
- #goalRevisionWarning(goal) {
586
- if (goal.parentId === null || goal.phase === "complete")
587
- return [];
588
- let root = this.#goal(goal.parentId);
589
- while (root.parentId !== null)
590
- root = this.#goal(root.parentId);
591
- const rootSeq = this.ledger.readStream(goalStream(root.id)).filter((event) => event.type === "goal.put").at(-1)?.seq ?? 0;
592
- const goalSeq = this.ledger.readStream(goalStream(goal.id)).filter((event) => event.type === "goal.put").at(-1)?.seq ?? 0;
593
- return rootSeq > goalSeq ? [`Goal ${goal.id} predates root revision ${root.revision}; CEO must revise its objective/observation method before new gated actions.`] : [];
594
- }
595
- #assertAgentGoalsCurrent(agent) {
596
- const warnings = this.ledger.goalsForOwner(agent).flatMap((goal) => this.#goalRevisionWarning(goal));
597
- if (warnings.length)
598
- throw new Error(warnings.join(" "));
599
- }
720
+ #assertGoalMutationAuthority(goal, actor) { if (goal.parentId === null) {
721
+ if (actor !== "human")
722
+ throw new Error("only human may modify a root goal");
723
+ return;
724
+ } const parent = this.ledger.goal(goal.parentId); if (parent?.owner !== actor)
725
+ throw new Error("only the parent goal owner may modify a child goal"); }
600
726
  #now() { return this.clock.now().toISOString(); }
601
- async #agentRpc(wake, turn, leaseToken, method, params) {
602
- const current = this.ledger.wake(wake.id);
603
- if (!current || current.status !== "running" || current.leaseToken !== leaseToken || !current.leaseUntil || current.leaseUntil < this.#now())
604
- throw new Error("stale runner RPC rejected");
605
- const profile = this.#profiles.get(wake.agent) ?? { agent: wake.agent, role: "child" };
727
+ #futureTime(value, after) { const parsed = Date.parse(value); if (!Number.isFinite(parsed) || parsed <= Date.parse(after))
728
+ throw new Error("nextWakeAt must be later than the Turn start"); return new Date(parsed).toISOString(); }
729
+ #runnerProfileId(agent) { return this.#profiles.get(agent)?.runnerProfile ?? "default"; }
730
+ #contextMail(target) { const selected = []; let used = 0; const matches = (mail) => target.bindingKind === "goal" ? mail.routeKind === "goal" && mail.goalId === target.goalId : target.bindingKind === "human" ? mail.routeKind === "human_inbox" : mail.routeKind === "specialist_inbox" && mail.specialistRole === target.specialistRole; for (const mail of this.ledger.unreadMail(target.agent).filter(matches)) {
731
+ const cost = Math.min(JSON.stringify(mail.body).length, 2_000) + 128;
732
+ if (selected.length >= 20 || selected.length > 0 && used + cost > this.#memoryTailChars)
733
+ break;
734
+ selected.push(mail);
735
+ used += cost;
736
+ } return selected; }
737
+ #mailSourceSeqs(mail) { const ids = new Set(mail.map((item) => item.id)); if (!ids.size)
738
+ return []; return this.ledger.eventsSince(0, ["mail.put"]).filter((event) => { const data = event.data && typeof event.data === "object" && !Array.isArray(event.data) ? event.data : {}; return typeof data.snapshot?.id === "string" && ids.has(data.snapshot.id); }).map((event) => event.seq); }
739
+ async #agentRpcForTurn(turnId, agent, context, method, params, sourceWakeId) {
740
+ const handoffAttemptId = method === "goal.handoff.validate" ? this.#beginHandoffAttempt(turnId) : null;
741
+ const execution = this.ledger.turn(turnId);
742
+ if (!execution || execution.status !== "in_progress" || !execution.leaseToken || !execution.leaseUntil || execution.leaseUntil < this.#now())
743
+ throw new Error("stale Turn RPC rejected");
744
+ if (context.goalBinding) {
745
+ const goal = this.ledger.goal(context.goalBinding.goalId);
746
+ const thread = this.ledger.thread(execution.threadId);
747
+ if (!goal || goal.phase !== "active" || goal.revision !== context.goalBinding.goalRevision || goal.owner !== agent || execution.bindingKind !== "goal" || execution.goalId !== goal.id || execution.goalRevision !== goal.revision || thread?.agent !== agent)
748
+ throw new Error("stale Goal-bound Turn RPC rejected");
749
+ }
750
+ const input = asRecord(params);
751
+ if (method === "goal.handoff.validate") {
752
+ const candidate = String(input.candidateMessage ?? "");
753
+ this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: agent, type: "rpc.goal.handoff.validate", data: { attemptId: handoffAttemptId, handoff: input.handoff ?? null, candidateMessagePresent: Boolean(candidate.trim()), candidateMessageChars: candidate.length }, ignorable: true });
754
+ return this.#validateHandoffDraft(turnId, handoffAttemptId, agent, execution, context, input);
755
+ }
756
+ const profile = this.#profiles.get(agent) ?? { agent, role: "child" };
606
757
  const allowed = new Set(profile.capabilities ?? defaultCapabilities(profile.role));
607
758
  if (!allowed.has(method))
608
759
  throw new Error(`${profile.role} agent is not allowed to call ${method}`);
609
- if (!turn.goalBinding && goalBoundCapability(method))
760
+ if (!context.goalBinding && goalBoundCapability(method))
610
761
  throw new Error(`${method} requires a Goal-bound Turn`);
611
- this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: `rpc.${method}`, data: params }, leaseToken);
612
- const input = asRecord(params);
762
+ this.ledger.appendEvent({ streamId: `turn:${turnId}`, ts: this.#now(), actor: agent, type: `rpc.${method}`, data: params, ignorable: true });
613
763
  if (method === "ledger.search")
614
764
  return this.ledger.searchEvents(String(input.query), Number(input.limit ?? 20));
615
765
  if (method === "team.list")
616
766
  return this.teamList();
617
767
  if (method === "goal.get")
618
- return (turn.goalBinding ? this.ledger.goal(turn.goalBinding.goalId) : null);
768
+ return (context.goalBinding ? this.ledger.goal(context.goalBinding.goalId) : null);
619
769
  if (method === "goal.create") {
620
- if (turn.source.kind !== "human" || turn.goalBinding)
621
- throw new Error("a Root Goal can only be created from an unbound Human Turn");
622
- if (wake.agent !== "ceo")
623
- throw new Error("only CEO may translate Human intent into a Root Goal");
624
- const goal = this.createRootGoal(String(input.objective), typeof input.id === "string" && input.id.trim() ? input.id : undefined);
625
- turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
626
- this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "turn.goal_bound", data: { goalId: goal.id, goalRevision: goal.revision, authority: "human" } }, leaseToken);
627
- return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
770
+ if (context.source.kind !== "human" || context.goalBinding || agent !== "ceo")
771
+ throw new Error("Root Goal creation requires an unbound CEO Human Turn");
772
+ const goal = this.createRootGoal(String(input.objective), typeof input.id === "string" ? input.id : undefined, turnId);
773
+ context.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
774
+ this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(goal.id, goal.revision) }, "supervisor");
775
+ return { goal, goalBinding: context.goalBinding };
628
776
  }
629
777
  if (method === "goal.work") {
630
- if (turn.source.kind !== "human" || turn.goalBinding)
778
+ if (context.source.kind !== "human" || context.goalBinding)
631
779
  throw new Error("work_on_goal requires an unbound Human Turn");
632
780
  const goal = this.#goal(String(input.goalId));
633
- if (goal.owner !== wake.agent || goal.phase !== "active")
634
- throw new Error("work_on_goal requires an active Goal owned by this Agent");
635
- turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
636
- this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "turn.goal_bound", data: { goalId: goal.id, goalRevision: goal.revision, authority: "human" } }, leaseToken);
637
- return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
781
+ if (goal.phase !== "active" || !this.#validGoalOwner(agent, goal))
782
+ throw new Error("work_on_goal requires the active owned Root Goal");
783
+ context.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
784
+ this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(goal.id, goal.revision) }, "supervisor");
785
+ return { goal, goalBinding: context.goalBinding };
638
786
  }
639
787
  if (method === "work_record.list")
640
788
  return this.ledger.workRecords();
641
789
  if (method === "work_record.read")
642
- return this.ledger.workRecord(String(input.goalId ?? turn.goalBinding?.goalId ?? ""));
790
+ return this.ledger.workRecord(String(input.goalId ?? context.goalBinding?.goalId ?? ""));
643
791
  if (method === "work_record.history")
644
- return this.ledger.workRecordHistory(String(input.goalId ?? turn.goalBinding?.goalId ?? ""));
792
+ return this.ledger.workRecordHistory(String(input.goalId ?? context.goalBinding?.goalId ?? ""));
645
793
  if (method === "work_record.diff")
646
- return this.ledger.workRecordDiff(String(input.goalId ?? turn.goalBinding?.goalId ?? ""), Number(input.fromRevision), Number(input.toRevision));
794
+ return this.ledger.workRecordDiff(String(input.goalId ?? context.goalBinding?.goalId ?? ""), Number(input.fromRevision), Number(input.toRevision));
647
795
  if (method === "work_record.search")
648
796
  return this.ledger.searchWorkRecords(String(input.query), Number(input.limit ?? 20));
649
797
  if (method === "work_record.update") {
650
- if (!turn.goalBinding)
651
- throw new Error("work_record.update requires a Goal-bound Turn");
652
- return this.ledger.updateWorkRecord({ goalId: turn.goalBinding.goalId, goalRevision: turn.goalBinding.goalRevision, expectedRevision: Number(input.expectedRevision), content: String(input.content), reason: String(input.reason), evidence: numberArray(input.evidence), turnId: wake.id, wakeId: wake.id }, wake.agent);
653
- }
654
- if (method === "goal.delegate")
655
- return this.delegate({
656
- id: String(input.id),
657
- parentGoalId: String(input.parentGoalId),
658
- childGoal: asChildGoal(input.childGoal),
659
- brief: (input.brief ?? null),
660
- reason: String(input.reason),
661
- evidence: numberArray(input.evidence),
662
- }, wake.agent, wake.id);
663
- if (method === "goal.reassign")
664
- return this.reassignGoal({
665
- id: String(input.id),
666
- goalId: String(input.goalId),
667
- newOwner: String(input.newOwner),
668
- brief: (input.brief ?? null),
669
- reason: String(input.reason),
670
- evidence: numberArray(input.evidence),
671
- }, wake.agent, wake.id);
798
+ const binding = context.goalBinding;
799
+ return this.ledger.updateWorkRecord({ goalId: binding.goalId, goalRevision: binding.goalRevision, expectedRevision: Number(input.expectedRevision), content: String(input.content), reason: String(input.reason), evidence: numberArray(input.evidence), turnId, ...(sourceWakeId ? { sourceWakeId } : {}) }, agent);
800
+ }
801
+ if (method === "goal.delegate") {
802
+ const binding = context.goalBinding;
803
+ if (String(input.parentGoalId) !== binding.goalId)
804
+ throw new Error("delegation parent must be the Goal bound to this Turn");
805
+ return this.delegate({ id: String(input.id), parentGoalId: binding.goalId, expectedParentRevision: Number(input.expectedParentRevision), childGoal: asChildGoal(input.childGoal), brief: (input.brief ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, agent, sourceWakeId);
806
+ }
807
+ if (method === "goal.reassign") {
808
+ const goal = this.#goal(String(input.goalId));
809
+ if (goal.parentId !== context.goalBinding.goalId)
810
+ throw new Error("reassignment target must be a child of the Goal bound to this Turn");
811
+ return await this.reassignGoal({ id: String(input.id), goalId: goal.id, expectedGoalRevision: Number(input.expectedGoalRevision), newOwner: String(input.newOwner), brief: (input.brief ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, agent, sourceWakeId);
812
+ }
672
813
  if (method === "goal.revise")
673
- return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), String(input.verificationMethod), wake.agent, String(input.reason), numberArray(input.evidence), wake.id);
814
+ return this.reviseChildGoal(String(input.goalId), String(input.objective), String(input.observationMethod), String(input.verificationMethod), agent, String(input.reason), numberArray(input.evidence), sourceWakeId, turnId);
674
815
  if (method === "goal.pause" || method === "goal.resume") {
675
- const goalId = String(input.goalId);
676
- const currentGoal = this.#goal(goalId);
677
- const directHumanRoot = !turn.goalBinding && turn.source.kind === "human" && currentGoal.parentId === null;
678
- if (!turn.goalBinding && !directHumanRoot)
816
+ const goal = this.#goal(String(input.goalId));
817
+ const directRoot = !context.goalBinding && context.source.kind === "human" && goal.parentId === null;
818
+ if (!context.goalBinding && !directRoot)
679
819
  throw new Error(`${method} requires a Goal-bound Turn`);
680
- const goal = this.transitionGoal(goalId, method === "goal.pause" ? "paused" : "active", directHumanRoot ? "human" : wake.agent);
681
- if (method === "goal.resume" && directHumanRoot) {
682
- turn.goalBinding = { goalId: goal.id, goalRevision: goal.revision };
683
- this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "turn.goal_bound", data: { goalId: goal.id, goalRevision: goal.revision, authority: "human" } }, leaseToken);
684
- return { goal, goalBinding: turn.goalBinding, instruction: "This Turn is now Goal-bound. Update its Work Record and finish with handoff." };
820
+ const next = this.transitionGoal(goal.id, method === "goal.pause" ? "paused" : "active", directRoot ? "human" : agent, !directRoot, turnId);
821
+ if (method === "goal.resume" && directRoot) {
822
+ context.goalBinding = { goalId: next.id, goalRevision: next.revision };
823
+ this.ledger.putTurn({ ...this.ledger.turn(turnId), ...goalTurnBinding(next.id, next.revision) }, "supervisor");
824
+ return { goal: next, goalBinding: context.goalBinding };
685
825
  }
686
- return goal;
826
+ return next;
687
827
  }
688
828
  if (method === "goal.complete") {
689
- const currentGoal = this.#goal(String(input.goalId));
690
- const directHumanRoot = !turn.goalBinding && turn.source.kind === "human" && currentGoal.parentId === null;
691
- if (!turn.goalBinding && !directHumanRoot)
829
+ const goal = this.#goal(String(input.goalId));
830
+ const directRoot = !context.goalBinding && context.source.kind === "human" && goal.parentId === null;
831
+ if (!context.goalBinding && !directRoot)
692
832
  throw new Error("goal.complete requires a Goal-bound Turn");
693
- return this.completeGoal({ goalId: currentGoal.id, revision: Number(input.revision), reason: String(input.reason), evidence: numberArray(input.evidence) }, directHumanRoot ? "human" : wake.agent, wake.id);
833
+ return this.completeGoal({ goalId: goal.id, revision: Number(input.revision), reason: String(input.reason), evidence: numberArray(input.evidence), sourceTurnId: turnId }, directRoot ? "human" : agent, sourceWakeId);
834
+ }
835
+ if (method === "mail.send") {
836
+ const to = String(input.to);
837
+ if (to === "human")
838
+ throw new Error("Human communication requires human.request");
839
+ if (!this.#knownAgent(to))
840
+ throw new Error(`unknown Agent recipient: ${to}`);
841
+ const goalId = String(input.goalId ?? "").trim();
842
+ if (!goalId)
843
+ throw new Error("Agent Mail requires a Goal route");
844
+ const goal = this.#goal(goalId);
845
+ if (!this.#validGoalOwner(to, goal))
846
+ throw new Error("Agent Mail Goal route does not match the recipient role and ownership");
847
+ const mail = { id: randomUUID(), to, from: agent, level: String(input.level), ...goalRoute(goalId), body: (input.body ?? null), readAt: null };
848
+ this.ledger.putMail(mail, agent, sourceWakeId);
849
+ return mail;
694
850
  }
695
851
  if (method === "human.request") {
852
+ if (agent !== "ceo")
853
+ throw new Error("only CEO may request Human input");
696
854
  const evidence = numberArray(input.evidence);
855
+ if (!evidence.length)
856
+ throw new Error("evidence is required");
697
857
  for (const seq of evidence)
698
858
  if (!this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
699
859
  throw new Error(`evidence event does not exist: ${seq}`);
700
- const mail = { id: randomUUID(), to: "human", from: wake.agent, level: "decision", body: { type: String(input.type ?? "decision"), message: input.message ?? null, evidence }, readAt: null };
701
- this.ledger.putMail(mail, wake.agent, wake.id);
702
- this.ledger.appendRunnerEvent({ streamId: wakeStream(wake.id), ts: this.#now(), actor: wake.agent, type: "ceo.human_requested", data: mail.body }, leaseToken);
860
+ const mail = { id: randomUUID(), to: "human", from: agent, level: "decision", ...humanRequestRoute(), body: { type: String(input.type), message: input.message ?? null, evidence }, readAt: null };
861
+ this.ledger.putMail(mail, agent, sourceWakeId);
703
862
  return mail;
704
863
  }
705
- if (method === "mail.send") {
706
- const mail = { id: randomUUID(), to: String(input.to), from: wake.agent, level: String(input.level), body: (input.body ?? null), readAt: null };
707
- this.ledger.putMail(mail, wake.agent, wake.id);
708
- return mail;
864
+ if (method === "schedule.set")
865
+ return (this.planWake(agent, String(input.at), String(input.reason), agent, context.goalBinding) ?? { scheduled: true });
866
+ if (method === "goal.put") {
867
+ const next = input.goal;
868
+ if (!this.#validGoalOwner(next.owner, next))
869
+ throw new Error("Goal owner role does not match Root/Child position");
870
+ const current = this.ledger.goal(next.id);
871
+ const operation = !current ? "create" : current.phase !== next.phase ? (next.phase === "paused" ? "pause" : next.phase === "active" ? "resume" : next.phase === "blocked" ? "block" : "complete") : current.owner !== next.owner ? "reassign" : "revise";
872
+ this.ledger.putGoal(next, agent, sourceWakeId, { operation, reason: String(input.reason ?? "Advanced Goal mutation"), evidence: numberArray(input.evidence ?? []), sourceTurnId: turnId, ...(sourceWakeId ? { sourceWakeId } : {}) });
873
+ return input.goal;
709
874
  }
710
875
  if (method === "memory.append") {
711
876
  const note = String(input.note ?? "").trim();
712
877
  if (!note)
713
878
  throw new Error("memory note cannot be empty");
714
- const record = this.ledger.appendEvent({ streamId: memoryStream(wake.agent), ts: this.#now(), actor: wake.agent, type: "memory.appended", data: { note, wakeId: wake.id } });
715
- return { seq: record.seq, streamSeq: record.streamSeq };
879
+ const event = this.ledger.appendEvent({ streamId: memoryStream(agent), ts: this.#now(), actor: agent, type: "memory.appended", data: { note, turnId } });
880
+ return { seq: event.seq };
716
881
  }
717
- if (method === "schedule.set")
718
- return (this.planWake(wake.agent, String(input.at), String(input.reason), wake.agent) ?? { scheduled: true });
719
- if (method === "audit.ack")
720
- return this.ackAuditAdvice(String(input.actionId), wake.agent);
721
- if (method === "audit.write")
722
- return this.putAuditAdvice(String(input.actionId), { by: wake.agent, body: (input.body ?? null), evidence: numberArray(input.evidence) }, wake.id);
723
- if (method === "goal.put") {
724
- this.ledger.putGoal(input.goal, wake.agent, wake.id);
725
- return input.goal;
726
- }
727
- const action = await this.submitAction({
728
- id: String(input.id), agent: wake.agent, kind: String(input.kind), payload: (input.payload ?? null), reason: String(input.reason), evidence: numberArray(input.evidence), auditAdvice: null, adviceAcked: false,
729
- }, String(input.connector), wake.id);
730
- return action;
882
+ throw new Error(`unsupported Agent capability: ${method}`);
731
883
  }
732
- async #collectMetrics() {
733
- const now = this.clock.now().getTime();
734
- for (const registration of this.#metricCollectors.values()) {
735
- if (registration.nextAt > now)
736
- continue;
737
- registration.nextAt = now + registration.intervalMs;
738
- const sample = await runJsonProcess(registration.spec, { goalId: registration.goalId });
739
- this.recordMetric({ ...sample, goalId: registration.goalId });
884
+ #validateHandoffDraft(turnId, attemptId, agent, execution, context, input) {
885
+ if (!context.goalBinding || execution.bindingKind !== "goal")
886
+ return { accepted: false, fatal: true, attemptId, issues: [{ code: "turn_not_goal_bound", message: "This Turn is no longer Goal-bound." }] };
887
+ const request = { handoff: (input.handoff ?? null), candidateMessage: String(input.candidateMessage ?? "") };
888
+ const issues = [];
889
+ try {
890
+ assertAgentHandoff(request.handoff);
740
891
  }
741
- }
742
- #scheduleMetricAlerts() {
743
- for (const [goalId, contract] of this.#metricContracts) {
744
- const goal = this.ledger.goal(goalId);
745
- if (!goal)
746
- continue;
747
- const samples = this.ledger.metricSamples(goal.id);
748
- const evaluation = evaluateMetric(contract, samples, this.#now());
749
- if (evaluation.shouldWakeOwner)
750
- this.#enqueueTrigger(goal.owner, `metric:${goal.id}:${evaluation.status}:${samples.at(-1)?.observedAt ?? "none"}`);
892
+ catch {
893
+ issues.push({ code: "handoff_invalid", message: "Handoff requires a valid outcome and at least one evidence event." });
751
894
  }
895
+ const existingMessage = [...this.ledger.turnItems(turnId)].reverse().find((item) => item.type === "assistant_message" && item.status === "completed" && typeof item.data.text === "string" && Boolean(item.data.text.trim()));
896
+ const message = request.candidateMessage.trim() || String(existingMessage?.data?.text ?? "").trim();
897
+ if (!message)
898
+ issues.push({ code: "message_missing", message: "This Goal Turn needs a readable assistant message before it can finish." });
899
+ const goal = this.ledger.goal(context.goalBinding.goalId);
900
+ if (!goal || goal.phase !== "active" || goal.owner !== agent || goal.revision !== context.goalBinding.goalRevision)
901
+ return { accepted: false, fatal: true, attemptId, issues: [{ code: "goal_fence_changed", message: "Goal ownership, phase, or revision changed; this Turn can no longer finish against the old binding." }] };
902
+ const record = this.ledger.workRecord(goal.id);
903
+ if (!record || record.updatedInTurn !== turnId || record.goalRevision !== goal.revision)
904
+ issues.push({ code: "work_record_not_updated", message: "Update this Goal's Work Record in the current Turn before Handoff.", details: { goalId: goal.id, recordRevision: record?.recordRevision ?? null } });
905
+ const evidence = Array.isArray(request.handoff?.evidence) ? request.handoff.evidence : [];
906
+ for (const seq of evidence)
907
+ if (!Number.isInteger(seq) || !this.ledger.eventsSince(seq - 1).some((event) => event.seq === seq))
908
+ issues.push({ code: "evidence_not_found", message: `Evidence event ${String(seq)} does not exist in the Ledger.`, details: { seq } });
909
+ if (issues.length)
910
+ return { accepted: false, fatal: false, attemptId, issues };
911
+ const token = randomUUID();
912
+ this.#handoffValidations.set(token, { turnId, attemptId, agent, attempt: execution.attempt, leaseToken: execution.leaseToken, goalId: goal.id, goalRevision: goal.revision, message, handoff: { outcome: request.handoff.outcome, evidence: [...request.handoff.evidence] } });
913
+ return { accepted: true, fatal: false, attemptId, token, goalId: goal.id, goalRevision: goal.revision };
752
914
  }
753
- /** The one mechanical floor: total ledger silence. Any event from anyone resets the clock; stall policy is the CEO's business, not the supervisor's. */
754
- #checkSystemSilence() {
755
- if (!this.#silence)
756
- return;
757
- const last = this.ledger.latestEvent();
758
- if (!last)
759
- return;
760
- const silentMs = this.clock.now().getTime() - Date.parse(last.ts);
761
- if (silentMs <= this.#silence.maxSilentMs)
762
- return;
763
- const fact = { silentMs, lastEventSeq: last.seq, lastEventAt: last.ts };
764
- this.ledger.appendEvent({ streamId: controlStream("watchdog"), ts: this.#now(), actor: "supervisor", type: "watchdog.system_silence", data: { ...fact, notify: this.#silence.notify } });
765
- this.ledger.putMail({ id: `silence:${last.seq}`, to: this.#silence.notify, from: "supervisor", level: "decision", readAt: null, body: { type: "system_silence", ...fact, note: "No ledger events system-wide within the silence window. Confirm this is expected and hand off." } }, "supervisor");
766
- }
915
+ #beginHandoffAttempt(turnId) { this.#invalidateHandoffTokens(turnId); const attemptId = (this.#handoffAttemptSeq.get(turnId) ?? 0) + 1; this.#handoffAttemptSeq.set(turnId, attemptId); return attemptId; }
916
+ #invalidateHandoffTokens(turnId) { for (const [token, value] of this.#handoffValidations)
917
+ if (value.turnId === turnId)
918
+ this.#handoffValidations.delete(token); }
919
+ #clearHandoffState(turnId) { this.#invalidateHandoffTokens(turnId); this.#handoffAttemptSeq.delete(turnId); }
767
920
  }
768
921
  export async function runSupervisorDaemon(supervisor, options = {}) {
769
922
  const pollMs = options.pollMs ?? 1_000;
@@ -776,8 +929,15 @@ export async function runSupervisorDaemon(supervisor, options = {}) {
776
929
  options.onError?.(error);
777
930
  }
778
931
  await new Promise((resolve) => {
779
- const timer = setTimeout(resolve, pollMs);
780
- options.signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
932
+ if (options.signal?.aborted) {
933
+ resolve();
934
+ return;
935
+ }
936
+ const onAbort = () => { clearTimeout(timer); resolve(); };
937
+ const timer = setTimeout(() => { options.signal?.removeEventListener("abort", onAbort); resolve(); }, pollMs);
938
+ options.signal?.addEventListener("abort", onAbort, { once: true });
939
+ if (options.signal?.aborted)
940
+ onAbort();
781
941
  });
782
942
  }
783
943
  }
@@ -785,103 +945,130 @@ export function deriveTeam(ledger, now = new Date().toISOString()) {
785
945
  const goals = ledger.goals();
786
946
  const wakes = ledger.wakes();
787
947
  const schedules = ledger.schedules();
948
+ const threads = ledger.threads();
949
+ const turns = ledger.turns();
788
950
  const handoffs = ledger.eventsSince(0, ["handoff.recorded"]);
789
951
  const owners = [...new Set(goals.map((goal) => goal.owner))].sort();
790
952
  return owners.map((agent) => {
791
953
  const owned = goals.filter((goal) => goal.owner === agent);
792
954
  const live = owned.filter((goal) => goal.phase !== "complete");
793
955
  const agentWakes = wakes.filter((wake) => wake.agent === agent).sort((a, b) => b.enqueuedSeq - a.enqueuedSeq);
794
- const activeWake = agentWakes.find((wake) => ["running", "leased", "queued"].includes(wake.status));
795
- const nextWakeAt = schedules.filter((schedule) => schedule.agent === agent && schedule.nextWakeAt > now).map((schedule) => schedule.nextWakeAt).sort()[0] ?? null;
956
+ const activeWake = agentWakes.find((wake) => { if (wake.status !== "claimed" && wake.status !== "queued")
957
+ return false; if (!wake.goalId)
958
+ return true; const goal = ledger.goal(wake.goalId); return goal?.phase === "active" && goal.owner === agent; });
959
+ const threadIds = new Set(threads.filter((thread) => thread.agent === agent).map((thread) => thread.id));
960
+ const activeTurn = turns.find((turn) => threadIds.has(turn.threadId) && turn.status === "in_progress");
961
+ const nextWakeAt = schedules.filter((schedule) => { if (schedule.agent !== agent || schedule.status !== "pending" || schedule.nextWakeAt <= now)
962
+ return false; if (!schedule.goalId)
963
+ return true; const goal = ledger.goal(schedule.goalId); return goal?.phase === "active" && goal.owner === agent; }).map((schedule) => schedule.nextWakeAt).sort()[0] ?? null;
796
964
  const lastHandoff = [...handoffs].reverse().find((event) => event.actor === agent) ?? null;
797
- const blocker = lastHandoff && typeof lastHandoff.data === "object" && lastHandoff.data !== null && !Array.isArray(lastHandoff.data)
798
- ? (lastHandoff.data.blocker ?? (lastHandoff.data.outcome === "blocked" ? "blocked" : null))
799
- : null;
800
- let status;
965
+ const value = lastHandoff?.data;
966
+ const lastOutcome = typeof value?.outcome === "string" ? value.outcome : null;
967
+ let motion;
801
968
  if (live.length === 0)
802
- status = "retired";
803
- else if (activeWake?.status === "running")
804
- status = "running";
969
+ motion = "retired";
970
+ else if (activeTurn)
971
+ motion = "running";
805
972
  else if (activeWake)
806
- status = "queued";
973
+ motion = "queued";
807
974
  else if (nextWakeAt)
808
- status = "scheduled";
809
- else if (live.every((goal) => goal.phase === "blocked"))
810
- status = "blocked";
811
- else if (live.every((goal) => goal.phase === "paused") || typeof blocker === "string" && blocker.length > 0)
812
- status = "waiting";
975
+ motion = "scheduled";
813
976
  else
814
- status = "idle_unplanned";
815
- return { agent, goalIds: owned.map((goal) => goal.id), status, lastHandoffSeq: lastHandoff?.seq ?? null, lastWakeStatus: agentWakes[0]?.status ?? null, nextWakeAt };
977
+ motion = "idle";
978
+ return { agent, goalIds: owned.map((goal) => goal.id), motion, lastOutcome, lastHandoffSeq: lastHandoff?.seq ?? null, lastWakeStatus: agentWakes[0]?.status ?? null, nextWakeAt };
816
979
  });
817
980
  }
981
+ export function deriveRecoveryViews(ledger) {
982
+ const views = [];
983
+ const wakes = ledger.wakes();
984
+ const schedules = ledger.schedules();
985
+ const triggers = wakes.flatMap((wake) => ledger.wakeTriggers(wake.id));
986
+ for (const turn of ledger.turns()) {
987
+ if (turn.status !== "failed")
988
+ continue;
989
+ const sourceWake = wakes.find((wake) => wake.turnId === turn.id);
990
+ const thread = ledger.thread(turn.threadId);
991
+ const goal = turn.bindingKind === "goal" ? ledger.goal(turn.goalId) : null;
992
+ const goalFailure = Boolean(turn.bindingKind === "goal" && thread && goal && goal.phase === "active" && goal.owner === thread.agent);
993
+ const specialistFailure = Boolean(turn.bindingKind === "specialist" && thread && sourceWake?.agent === thread.agent);
994
+ if (!sourceWake || !thread || !goalFailure && !specialistFailure)
995
+ continue;
996
+ const base = { turnId: turn.id, agent: thread.agent };
997
+ const scheduled = schedules.some((schedule) => {
998
+ const recovery = parseRecoveryTrigger(schedule.id);
999
+ const sameTarget = goalFailure ? schedule.bindingKind === "goal" && schedule.goalId === goal.id : schedule.bindingKind === "specialist" && turn.bindingKind === "specialist" && schedule.specialistRole === turn.specialistRole;
1000
+ return schedule.status === "pending" && schedule.setBy === "supervisor" && schedule.agent === thread.agent && sameTarget && schedule.reason === recoveryRef(turn.id) && recovery?.turnId === turn.id && recovery.attempt > 0;
1001
+ });
1002
+ if (scheduled) {
1003
+ views.push({ ...base, state: "scheduled", actionable: false });
1004
+ continue;
1005
+ }
1006
+ const recoveryWake = triggers.flatMap((trigger) => {
1007
+ const recovery = parseRecoveryTrigger(trigger.triggerRef);
1008
+ const wake = recovery?.turnId === turn.id ? wakes.find((candidate) => candidate.id === trigger.wakeId) : null;
1009
+ const sameTarget = goalFailure ? wake?.bindingKind === "goal" && wake.goalId === goal.id : wake?.bindingKind === "specialist" && turn.bindingKind === "specialist" && wake.specialistRole === turn.specialistRole;
1010
+ return wake?.agent === thread.agent && sameTarget ? [wake] : [];
1011
+ }).sort((left, right) => right.enqueuedSeq - left.enqueuedSeq)[0];
1012
+ if (recoveryWake?.status === "queued" || recoveryWake?.status === "claimed") {
1013
+ views.push({ ...base, state: "queued", actionable: false });
1014
+ continue;
1015
+ }
1016
+ if (recoveryWake?.status === "consumed" && recoveryWake.turnId) {
1017
+ const retry = ledger.turn(recoveryWake.turnId);
1018
+ const state = retry?.status === "in_progress" ? "running" : retry?.status === "completed" ? "recovered" : "superseded";
1019
+ views.push({ ...base, state, actionable: false });
1020
+ continue;
1021
+ }
1022
+ const parent = goalFailure && goal.parentId ? ledger.goal(goal.parentId) : null;
1023
+ const escalation = triggers.flatMap((trigger) => {
1024
+ const wake = trigger.triggerRef === childRetryExhaustedRef(turn.id) ? wakes.find((candidate) => candidate.id === trigger.wakeId) : null;
1025
+ return wake?.bindingKind === "goal" && parent && wake.agent === parent.owner && wake.goalId === parent.id && parent.phase === "active" ? [wake] : [];
1026
+ }).find((wake) => wake.status === "queued" || wake.status === "claimed" || wake.status === "consumed");
1027
+ if (escalation) {
1028
+ views.push({ ...base, state: "escalated", actionable: false });
1029
+ continue;
1030
+ }
1031
+ views.push({ ...base, state: "needed", actionable: true });
1032
+ }
1033
+ return views;
1034
+ }
818
1035
  export function renderDashboard(ledger) {
819
1036
  const rows = (values) => values.map((value) => `<tr><td><pre>${escapeHtml(JSON.stringify(value, null, 2))}</pre></td></tr>`).join("");
820
- return `<!doctype html><html><head><meta charset="utf-8"><title>goah status</title><style>body{font:14px ui-monospace;margin:32px;background:#101418;color:#dce3e4}section{margin:32px 0}pre{white-space:pre-wrap;border:1px solid #334;padding:12px}</style></head><body><h1>goah</h1><p>seq ${ledger.events().at(-1)?.seq ?? 0}</p><section><h2>Team</h2><table>${rows(deriveTeam(ledger))}</table></section><section><h2>Goals</h2><table>${rows(ledger.goals())}</table></section><section><h2>Wakes</h2><table>${rows(ledger.wakes())}</table></section><section><h2>Actions</h2><table>${rows(ledger.actions())}</table></section><section><h2>Mailbox</h2><table>${rows(ledger.mailbox())}</table></section></body></html>`;
821
- }
822
- async function runConnector(spec, operation, action) {
823
- return runJsonProcess(spec, { operation, action });
824
- }
825
- async function runJsonProcess(spec, input) {
826
- const child = spawn(spec.command, spec.args, { detached: process.platform !== "win32", env: minimalEnvironment(spec.env), stdio: ["pipe", "pipe", "pipe"] });
827
- let stdout = "";
828
- let stderr = "";
829
- child.stdout.on("data", (chunk) => { stdout += chunk.toString(); });
830
- child.stderr.on("data", (chunk) => { stderr += chunk.toString(); });
831
- child.stdin.end(`${JSON.stringify(input)}\n`);
832
- const timeoutMs = spec.timeoutMs ?? 30_000;
833
- let timer;
834
- const exit = new Promise((resolve, reject) => {
835
- child.once("error", reject);
836
- child.once("close", (code, signal) => resolve({ code, signal }));
837
- });
838
- timer = setTimeout(() => { void terminateChild(child, 500); }, timeoutMs);
839
- const result = await exit;
840
- clearTimeout(timer);
841
- if (result.code !== 0)
842
- throw new Error(stderr.trim() || `connector exited (${result.code ?? result.signal})`);
843
- return JSON.parse(stdout.trim());
844
- }
845
- async function terminateChild(child, graceMs) {
846
- if (!child.pid || child.exitCode !== null || child.signalCode !== null)
847
- return;
848
- signalPid(child.pid, "SIGTERM");
849
- await new Promise((resolve) => setTimeout(resolve, graceMs));
850
- if (child.exitCode === null && child.signalCode === null)
851
- signalPid(child.pid, "SIGKILL");
852
- }
853
- function signalPid(pid, signal) { try {
854
- process.kill(process.platform === "win32" ? pid : -pid, signal);
855
- }
856
- catch { } }
857
- function minimalEnvironment(explicit = {}) {
858
- const env = {};
859
- for (const name of ["PATH", "TMPDIR", "TMP", "TEMP", "SYSTEMROOT"])
860
- if (process.env[name] !== undefined)
861
- env[name] = process.env[name];
862
- return { ...env, ...explicit };
1037
+ return `<!doctype html><html><head><meta charset="utf-8"><title>goah status</title><style>body{font:14px ui-monospace;margin:32px;background:#101418;color:#dce3e4}section{margin:32px 0}pre{white-space:pre-wrap;border:1px solid #334;padding:12px}</style></head><body><h1>goah</h1><p>seq ${ledger.events().at(-1)?.seq ?? 0}</p><section><h2>Team</h2><table>${rows(deriveTeam(ledger))}</table></section><section><h2>Goals</h2><table>${rows(ledger.goals())}</table></section><section><h2>Wakes</h2><table>${rows(ledger.wakes())}</table></section><section><h2>Mailbox</h2><table>${rows(ledger.mailbox())}</table></section></body></html>`;
863
1038
  }
864
1039
  function escapeHtml(value) { return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;"); }
865
- function interactionMailId(wake) { return wake.triggerRef.startsWith("interaction:") ? wake.triggerRef.slice("interaction:".length) : null; }
866
- function handoffBlocked(handoff) { return "goalId" in handoff ? handoff.outcome === "blocked" : Boolean(handoff.blocker); }
867
- function handoffTriggersCeo(handoff) { return "goalId" in handoff ? handoff.outcome === "blocked" || handoff.outcome === "completion_proposed" : Boolean(handoff.material || handoff.blocker); }
868
- function goalBoundCapability(method) { return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "mail.send", "schedule.set", "human.request"].includes(method); }
1040
+ function messageTextContent(content) { if (typeof content === "string")
1041
+ return content; if (!Array.isArray(content))
1042
+ return ""; return content.map((part) => part && typeof part === "object" && !Array.isArray(part) && part.type === "text" ? String(part.text ?? "") : "").filter(Boolean).join("\n"); }
1043
+ function goalBoundCapability(method) { return ["goal.delegate", "goal.reassign", "goal.revise", "goal.put", "work_record.update", "schedule.set", "human.request"].includes(method); }
869
1044
  function asRecord(value) { if (typeof value !== "object" || value === null || Array.isArray(value))
870
1045
  throw new Error("RPC params must be an object"); return value; }
1046
+ function boundedJson(value, maxChars = 2_000) { const text = typeof value === "string" ? value : JSON.stringify(value); return text.length <= maxChars ? text : `${text.slice(0, maxChars)}…`; }
1047
+ function recoveryRef(turnId) { return `recovery:${turnId}`; }
1048
+ function recoveryScheduleId(turnId, attempt) { return `${recoveryRef(turnId)}:${attempt}`; }
1049
+ function childRetryExhaustedRef(turnId) { return `child-retry-exhausted:${turnId}`; }
1050
+ function childRetryExhaustedTurnId(triggerRef) { return triggerRef.startsWith("child-retry-exhausted:") ? triggerRef.slice("child-retry-exhausted:".length) || null : null; }
1051
+ function parseRecoveryTrigger(triggerRef) { if (!triggerRef.startsWith("recovery:"))
1052
+ return null; const [turnId, rawAttempt] = triggerRef.slice("recovery:".length).split("@")[0].split(":"); if (!turnId)
1053
+ return null; const attempt = rawAttempt === undefined ? 0 : Number(rawAttempt); return Number.isInteger(attempt) && attempt >= 0 ? { turnId, attempt } : null; }
1054
+ function mailIdFromTriggerRef(triggerRef) { if (!triggerRef.startsWith("mail:"))
1055
+ return null; return triggerRef.slice("mail:".length).split("@redelivery:")[0] || null; }
1056
+ function mailDeliveryAttempt(triggerRef, base) { if (triggerRef === base)
1057
+ return 0; const prefix = `${base}@redelivery:`; if (!triggerRef.startsWith(prefix))
1058
+ return null; const attempt = Number(triggerRef.slice(prefix.length)); return Number.isInteger(attempt) && attempt > 0 ? attempt : null; }
871
1059
  function numberArray(value) { if (!Array.isArray(value) || value.some((item) => typeof item !== "number"))
872
1060
  throw new Error("RPC evidence must be a number array"); return value; }
1061
+ function isSameNumbers(left, right) { return left.length === right.length && left.every((value, index) => value === right[index]); }
873
1062
  function asChildGoal(value) {
874
1063
  const input = asRecord(value ?? null);
875
1064
  return { id: String(input.id), objective: String(input.objective), observationMethod: String(input.observationMethod), verificationMethod: String(input.verificationMethod), owner: String(input.owner) };
876
1065
  }
877
1066
  function defaultCapabilities(role) {
878
1067
  if (role === "ceo")
879
- return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "team.list", "goal.get", "goal.create", "goal.work", "goal.delegate", "goal.reassign", "goal.revise", "goal.pause", "goal.resume", "goal.complete", "human.request", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
880
- if (role === "verifier")
881
- return ["ledger.search", "mail.send", "memory.append", "audit.write"];
882
- if (role === "audit")
883
- return ["ledger.search", "mail.send", "memory.append", "audit.write"];
884
- return ["ledger.search", "mail.send", "schedule.set", "action.submit", "audit.ack", "goal.get", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
1068
+ return ["ledger.search", "mail.send", "schedule.set", "team.list", "goal.get", "goal.create", "goal.work", "goal.delegate", "goal.reassign", "goal.revise", "goal.pause", "goal.resume", "goal.complete", "human.request", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
1069
+ if (role === "verifier" || role === "audit")
1070
+ return ["ledger.search", "mail.send", "memory.append"];
1071
+ return ["ledger.search", "mail.send", "schedule.set", "goal.get", "work_record.list", "work_record.read", "work_record.history", "work_record.diff", "work_record.search", "work_record.update"];
885
1072
  }
886
1073
  export * from "./verification.js";
887
1074
  export * from "./roles.js";