@karma42/agent 0.1.0-beta.10 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Karma42 MCP connector
2
2
 
3
- `0.1.0-beta.10` is a **beta release**. It connects an existing MCP-capable AI client to a Karma42 project. Registration and current-chat mode do not select, download or run a model. An explicitly approved local worker is a separate work mode. An MCP-capable client using DeepSeek or another model can use the same transport if that client supports local stdio MCP and the required project tools.
3
+ `0.1.0-beta.11` is a **beta release**. It connects an existing MCP-capable AI client to a Karma42 project. Registration and current-chat mode do not select, download or run a model. An explicitly approved local worker is a separate work mode. An MCP-capable client using DeepSeek or another model can use the same transport if that client supports local stdio MCP and the required project tools.
4
4
 
5
5
  ## First-time setup
6
6
 
7
7
  Once per computer and client, print its registration instructions:
8
8
 
9
9
  ```sh
10
- npx --ignore-scripts @karma42/agent@0.1.0-beta.10 mcp-config --client claude
10
+ npx --ignore-scripts @karma42/agent@0.1.0-beta.11 mcp-config --client claude
11
11
  ```
12
12
 
13
13
  Use `--client codex` for Codex, or `--client other` for the generic stdio configuration. **Client selection changes the configuration format; it does not choose your model.**
@@ -106,6 +106,6 @@ CLI equivalents: `project-agents --project PROJECT_ID` and `agents-register --pr
106
106
 
107
107
  ## Bundled orchestration skill
108
108
 
109
- `skills/karma42/SKILL.md` is the single maintained workflow for owner access, project readiness/start, team selection/registration, batch approval, identity checks and supervised workers. The MCP supplies it as initialization instructions and through `get_karma42_guide`, so guidance is available even without native skill support. `npx --ignore-scripts @karma42/agent@0.1.0-beta.10 skill` prints the same file; it never edits settings. A client can optionally save it in its supported skill location after the user approves that file addition. Installation/registration does not grant account access or authorize work. A generic host subagent is not automatically an independently connected Karma42 worker.
109
+ `skills/karma42/SKILL.md` is the single maintained workflow for owner access, project readiness/start, team selection/registration, batch approval, identity checks and supervised workers. The MCP supplies it as initialization instructions and through `get_karma42_guide`, so guidance is available even without native skill support. `npx --ignore-scripts @karma42/agent@0.1.0-beta.11 skill` prints the same file; it never edits settings. A client can optionally save it in its supported skill location after the user approves that file addition. Installation/registration does not grant account access or authorize work. A generic host subagent is not automatically an independently connected Karma42 worker.
110
110
 
111
111
  First-time `mcp-config` instructions now offer the skill alongside registration. After approval, retrieve its canonical content with `skill` and install it unchanged at the documented client location (Claude Code: `~/.claude/skills/karma42/SKILL.md`; Codex: `~/.agents/skills/karma42/SKILL.md`). Existing different skills require separate replacement approval. Other clients use their documented skill support or `get_karma42_guide`. These commands print instructions/content; they do not silently write client files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karma42/agent",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.11",
4
4
  "description": "Model-independent Karma42 MCP connector for project agents",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,9 +1,13 @@
1
1
  // Runtime control stays outside model turns. One request at a time; fatal refusals stop the model.
2
2
  import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { TurnReportJournal } from './turn-report-journal.mjs';
3
4
 
4
5
  export class ControlV2 {
5
- constructor(url, token, { pollSeconds = 20, takeover = false, runtime, requestSignal, onFatal = () => {}, log = () => {} } = {}) {
6
+ constructor(url, token, { pollSeconds = 20, takeover = false, runtime, requestSignal, stateDir, agentId, onFatal = () => {}, log = () => {} } = {}) {
6
7
  Object.assign(this, { url, token, pollSeconds: Math.max(10, pollSeconds), takeover, runtime, requestSignal, onFatal, log });
8
+ this.turnReports = stateDir ? new TurnReportJournal(stateDir, { url, token, agentId }) : null;
9
+ this.reportFailure = null;
10
+ this.reportStorageFailure = null;
7
11
  this.phase = 'idle';
8
12
  this.lastPhaseAt = 0;
9
13
  this.lastTurn = undefined;
@@ -35,9 +39,12 @@ export class ControlV2 {
35
39
  }
36
40
 
37
41
  async start() {
42
+ await this.flushTurnReports();
38
43
  const run = await this.request('/runs', { runner_version: this.runtime ? `kit-${this.runtime.kitHash.slice(0, 12)}` : 'kit-v2', takeover: this.takeover, ...(this.runtime ? { runtime: this.runtime } : {}) });
39
44
  if (run.protocol !== 2) throw new Error('Protocol mismatch: this runner needs protocol 2.');
40
45
  this.runId = run.run_id;
46
+ this.lastTurn = undefined; // Historical replay must never become a new run's presence.
47
+ this.turnReports?.activateRun(this.runId);
41
48
  this.pollSeconds = Math.max(this.pollSeconds, run.poll_seconds);
42
49
  while (!this.closed && !this.news) {
43
50
  const delay = await this.observe();
@@ -52,13 +59,63 @@ export class ControlV2 {
52
59
  return task;
53
60
  }
54
61
 
62
+ reportDeliveryStatus() {
63
+ let capacityFailure;
64
+ try { this.turnReports?.assertCapacity(this.runId); } catch (error) { capacityFailure = error; }
65
+ return { pending: this.turnReports?.data.pending.length ?? 0, blocked: Boolean(this.reportStorageFailure || this.reportFailure?.fatal || capacityFailure),
66
+ error: this.reportStorageFailure?.message ?? this.reportFailure?.message ?? capacityFailure?.message ?? null };
67
+ }
68
+
69
+ assertReportCapacity() {
70
+ if (this.reportStorageFailure) throw this.reportStorageFailure;
71
+ if (this.reportFailure?.fatal) throw this.reportFailure;
72
+ this.turnReports?.assertCapacity(this.runId);
73
+ }
74
+
75
+ recordTurn(lastTurn) {
76
+ if (this.reportStorageFailure) throw this.reportStorageFailure;
77
+ try {
78
+ this.lastTurn = this.turnReports ? this.turnReports.add(this.runId, lastTurn) : lastTurn;
79
+ return this.lastTurn;
80
+ } catch (error) { this.reportStorageFailure = error; throw error; }
81
+ }
82
+
83
+ async deliverTurnReports() {
84
+ if (!this.turnReports || this.reportStorageFailure) return;
85
+ for (const entry of [...this.turnReports.data.pending]) {
86
+ try {
87
+ const receipt = await this.request('/turn-reports', { run_id: entry.run_id, last_turn: entry.last_turn });
88
+ if (receipt.ok !== true || typeof receipt.report_id !== 'string' || !receipt.report_id) throw new Error('Completion report acknowledgement is missing.');
89
+ try { this.turnReports.acknowledge(entry); }
90
+ catch (error) { this.reportStorageFailure = error; throw error; }
91
+ this.reportFailure = null;
92
+ } catch (error) {
93
+ this.reportFailure = error;
94
+ this.log(`Completed turn report retained locally: ${error.message}`);
95
+ break;
96
+ }
97
+ }
98
+ }
99
+
100
+ flushTurnReports() { return this.serialize(() => this.deliverTurnReports()); }
101
+
102
+ liveLastTurn() {
103
+ // Historical delivery never changes live presence. Publish the latest observation
104
+ // only after its journal acknowledgement, so phase reporting cannot bypass a refusal.
105
+ if (!this.lastTurn) return undefined;
106
+ const pending = this.turnReports?.data.pending.some(entry => entry.run_id === this.runId
107
+ && entry.last_turn.usage.sequence === this.lastTurn.usage?.sequence);
108
+ return pending ? undefined : this.lastTurn;
109
+ }
110
+
55
111
  async cycle() {
56
112
  return this.serialize(async () => {
113
+ await this.deliverTurnReports();
57
114
  this.news = null; // Fail closed while contact is uncertain: no new model turn.
58
115
  const reportDue = Date.now() - this.lastPhaseAt >= 60_000;
59
116
  const news = await this.request('/wait', {
60
117
  run_id: this.runId,
61
- ...(reportDue ? { report: { phase: this.phase, ...(this.lastTurn ? { last_turn: this.lastTurn } : {}),
118
+ ...(reportDue ? { report: { phase: this.phase, ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}),
62
119
  ...(this.activity ? { activity: this.activity } : {}) } } : {}),
63
120
  });
64
121
  // Advance only after the combined request succeeds. A lost response retries the report;
@@ -106,12 +163,13 @@ export class ControlV2 {
106
163
  }
107
164
 
108
165
  async setPhase(phase, lastTurn, reportRuntime = false) {
109
- if (lastTurn) this.lastTurn = lastTurn;
166
+ if (lastTurn && lastTurn !== this.lastTurn) this.recordTurn(lastTurn);
110
167
  if (this.fatal) throw this.fatal;
111
168
  this.phase = phase;
112
169
  return this.serialize(async () => {
170
+ await this.deliverTurnReports();
113
171
  await this.request(`/runs/${this.runId}/phase`, {
114
- phase, ...(this.lastTurn ? { last_turn: this.lastTurn } : {}), ...(this.activity ? { activity: this.activity } : {}), ...(reportRuntime && this.runtime ? { runtime: this.runtime } : {}),
172
+ phase, ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}), ...(this.activity ? { activity: this.activity } : {}), ...(reportRuntime && this.runtime ? { runtime: this.runtime } : {}),
115
173
  });
116
174
  this.lastPhaseAt = Date.now();
117
175
  });
@@ -124,7 +182,7 @@ export class ControlV2 {
124
182
  this.abort.abort();
125
183
  if (this.runId && (!this.fatal || this.fatal.code === 'runtime_update_required')) {
126
184
  try {
127
- await this.serialize(() => this.request(`/runs/${this.runId}/phase`, { phase: 'stopping', ...(reason ? { reason } : {}), ...(this.lastTurn ? { last_turn: this.lastTurn } : {}) }));
185
+ await this.serialize(async () => { await this.deliverTurnReports(); return this.request(`/runs/${this.runId}/phase`, { phase: 'stopping', ...(reason ? { reason } : {}), ...(this.liveLastTurn() ? { last_turn: this.liveLastTurn() } : {}) }); });
128
186
  } catch (error) { this.log(`Final stop report could not be saved: ${error.message}`); }
129
187
  }
130
188
  await this.loop;
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "source": "pilot/agent-kit",
3
3
  "files": {
4
- "control-v2.mjs": "02e325b642669b8f29c7b25582b1afaf4b74cbc63d2b463c11bf02abac90ca12",
4
+ "control-v2.mjs": "1140995d6869287026c38b763e160741ab08520749f8b2eb9b7cb3f931583ab1",
5
+ "turn-report-journal.mjs": "cdb64fe7ffea9e441e0e1a4572b128195740340c08d9501436e42244fbef8e3c",
5
6
  "work-controller.mjs": "ca1b6c7ad62d23ee90012e37c0218303bdd4e6b67c26714996b14c7f64e4269c",
6
7
  "providers.mjs": "bb411985eb7e6936856b6905f6efa7b9bdd48f9a08e0de8e4ceb785945cd69f2",
7
8
  "claude-settings.json": "bf3bfaaf38de37cda60a7dc48cb8c186c1864df61bd5ff500715ee2d1db4333e",
@@ -0,0 +1,81 @@
1
+ // Completed telemetry is independent of model/session recovery. Single foreground writer.
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { constants, fstatSync, mkdirSync, lstatSync, readdirSync, readFileSync, openSync, writeFileSync, fsyncSync, closeSync, renameSync, rmSync } from 'node:fs';
4
+ import { join, dirname, basename } from 'node:path';
5
+ const hash = value => createHash('sha256').update(value).digest('hex');
6
+ const MAX_BYTES = 2 * 1024 * 1024, MAX_PENDING = 128, MAX_RUNS = 128;
7
+ const invalid = () => new Error('Completion report journal is invalid or belongs to another connection; inspect private state before continuing.');
8
+ export class TurnReportJournal {
9
+ constructor(stateDir, { url, token, agentId = '' }) {
10
+ mkdirSync(stateDir, { recursive: true, mode: 0o700 });
11
+ const directoryStat = lstatSync(stateDir);
12
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw invalid();
13
+ this.identity = hash(JSON.stringify([url, agentId, token]));
14
+ this.file = join(stateDir, `turn-reports-${this.identity}.json`);
15
+ this.data = { version: 1, identity: this.identity, revision: 0, sequences: {}, pending: [] };
16
+ // Recover a completely flushed draft from a crash before rename. Never erase corrupt state.
17
+ const candidates = readdirSync(stateDir).filter(name => name === basename(this.file) || name.startsWith(`${basename(this.file)}.`) && name.endsWith('.tmp'));
18
+ for (const name of candidates) {
19
+ const path = join(stateDir, name);
20
+ let data, fd;
21
+ try {
22
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
23
+ const stat = fstatSync(fd);
24
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > MAX_BYTES || (stat.mode & 0o077)) throw invalid();
25
+ data = JSON.parse(readFileSync(fd, 'utf8'));
26
+ } catch { throw invalid(); } finally { if (fd !== undefined) closeSync(fd); }
27
+ this.validate(data);
28
+ if (data.revision === this.data.revision && JSON.stringify(data) !== JSON.stringify(this.data)) throw invalid();
29
+ if (data.revision > this.data.revision) this.data = data;
30
+ }
31
+ if (candidates.length) { this.save(); for (const name of candidates) if (name !== basename(this.file)) rmSync(join(stateDir, name)); this.syncDirectory(); }
32
+ }
33
+ validate(data) {
34
+ if (data?.version !== 1 || data.identity !== this.identity || !Number.isSafeInteger(data.revision) || data.revision < 0 || !data.sequences || Array.isArray(data.sequences) || !Array.isArray(data.pending) || data.pending.length > MAX_PENDING || Object.keys(data.sequences).length > MAX_RUNS) throw invalid();
35
+ for (const [run, seq] of Object.entries(data.sequences)) if (!run || !Number.isSafeInteger(seq) || seq < 1) throw invalid();
36
+ const ids = new Set();
37
+ for (const entry of data.pending) {
38
+ if (typeof entry.run_id !== 'string' || !entry.run_id || !Object.hasOwn(data.sequences, entry.run_id) || !Number.isSafeInteger(entry.last_turn?.usage?.sequence) || entry.last_turn.usage.sequence < 1 || entry.last_turn.usage.sequence > data.sequences[entry.run_id] || entry.fingerprint !== hash(JSON.stringify(entry.last_turn)) || ids.has(`${entry.run_id}:${entry.last_turn.usage.sequence}`)) throw invalid();
39
+ ids.add(`${entry.run_id}:${entry.last_turn.usage.sequence}`);
40
+ }
41
+ }
42
+ syncDirectory() { const fd = openSync(dirname(this.file), 'r'); try { fsyncSync(fd); } finally { closeSync(fd); } }
43
+ save() {
44
+ const serialized = JSON.stringify(this.data);
45
+ if (Buffer.byteLength(serialized) > MAX_BYTES) throw new Error('Completion report journal is full; deliver pending reports before more work.');
46
+ const draft = `${this.file}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
47
+ const fd = openSync(draft, 'wx', 0o600);
48
+ try { writeFileSync(fd, serialized); fsyncSync(fd); } finally { closeSync(fd); }
49
+ renameSync(draft, this.file); this.syncDirectory();
50
+ }
51
+ activateRun(runId) {
52
+ // Production only starts freshly issued runs. Completed older runs cannot allocate
53
+ // another sequence; retain every run that still owns an undelivered report.
54
+ const pendingRuns = new Set(this.data.pending.map(entry => entry.run_id));
55
+ const next = structuredClone(this.data);
56
+ for (const run of Object.keys(next.sequences)) if (run !== runId && !pendingRuns.has(run)) delete next.sequences[run];
57
+ if (JSON.stringify(next.sequences) === JSON.stringify(this.data.sequences)) return;
58
+ next.revision++; const previous = this.data; this.data = next;
59
+ try { this.save(); } catch (error) { this.data = previous; throw error; }
60
+ }
61
+ assertCapacity(runId) {
62
+ if (this.data.pending.length >= MAX_PENDING || !this.data.sequences[runId] && Object.keys(this.data.sequences).length >= MAX_RUNS || Buffer.byteLength(JSON.stringify(this.data)) > MAX_BYTES - 32 * 1024) throw new Error('Completion report journal is full; deliver pending reports before more work.');
63
+ }
64
+ add(runId, lastTurn) {
65
+ this.assertCapacity(runId);
66
+ const turn = structuredClone(lastTurn);
67
+ if (!turn.usage || typeof runId !== 'string' || !runId) throw new Error('A completion report needs an issued run and usage.');
68
+ turn.usage.sequence = (this.data.sequences[runId] ?? 0) + 1;
69
+ const next = structuredClone(this.data); next.revision++; next.sequences[runId] = turn.usage.sequence;
70
+ next.pending.push({ run_id: runId, last_turn: turn, fingerprint: hash(JSON.stringify(turn)) });
71
+ const previous = this.data; this.data = next;
72
+ try { this.save(); } catch (error) { this.data = previous; throw error; }
73
+ return structuredClone(turn); // Caller observations cannot mutate the durable upload payload.
74
+ }
75
+ acknowledge(entry) {
76
+ const next = structuredClone(this.data); next.revision++;
77
+ next.pending = next.pending.filter(item => !(item.run_id === entry.run_id && item.last_turn.usage.sequence === entry.last_turn.usage.sequence));
78
+ const previous = this.data; this.data = next;
79
+ try { this.save(); } catch (error) { this.data = previous; throw error; }
80
+ }
81
+ }
@@ -260,7 +260,8 @@ export async function serveReusable({ home = sessionHome(), input = process.stdi
260
260
  const current = context.worker?.status();
261
261
  if (current && (current.state !== 'idle' || current.mission_ref)) return current;
262
262
  const recovery = context.record ? workerRecoveryStatus(connectionDir(home, context.record.id), context.record) : null;
263
- return recovery ? { state: 'recovery_required', active: false, recovery } : current ?? { state: 'not_started', active: false };
263
+ const status = recovery ? { state: 'recovery_required', active: false, recovery } : current ?? { state: 'not_started', active: false };
264
+ return { ...status, report_delivery: context.ready?.control?.reportDeliveryStatus?.() ?? null };
264
265
  }
265
266
  async function startWorker(args) {
266
267
  if (Object.keys(args).some(key => !['connection_ref', 'client', 'model', 'minutes', 'scope', 'folder', 'approved', 'recovery_ref'].includes(key))) throw new Error('Use only the documented worker options.');
@@ -31,7 +31,7 @@ export async function serveSession({ connection, home = sessionHome(), input = p
31
31
  active = withLock(dir, async () => {
32
32
  try {
33
33
  requestAbort = new AbortController();
34
- control = new Control(record.service, token, { takeover: false, requestSignal: requestAbort.signal, onFatal: stop });
34
+ control = new Control(record.service, token, { stateDir: dir, agentId: record.agentId, takeover: false, requestSignal: requestAbort.signal, onFatal: stop });
35
35
  const startupTimeout = setTimeout(() => { requestAbort.abort(); control.abort.abort(); }, 30_000);
36
36
  try { await control.start(); control.loop?.catch(() => {}); } finally { clearTimeout(startupTimeout); }
37
37
  if (stopped) throw new Error('Client disconnected.');
package/src/system.mjs CHANGED
@@ -26,7 +26,7 @@ export function verifyBundle() {
26
26
  let manifest;
27
27
  try { manifest = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8')); }
28
28
  catch { throw new Error('Packaged runtime missing. Build or reinstall this package before preparing an agent.'); }
29
- const names = ['control-v2.mjs', 'work-controller.mjs', 'providers.mjs', 'claude-settings.json', 'AGENT_PROMPT_V2.md', 'project-tools.json'];
29
+ const names = ['control-v2.mjs', 'turn-report-journal.mjs', 'work-controller.mjs', 'providers.mjs', 'claude-settings.json', 'AGENT_PROMPT_V2.md', 'project-tools.json'];
30
30
  if (Object.keys(manifest.files ?? {}).sort().join() !== [...names].sort().join()) throw new Error('Invalid packaged runtime manifest.');
31
31
  const files = {};
32
32
  for (const name of names) {
package/src/version.mjs CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.0-beta.10';
1
+ export const VERSION = '0.1.0-beta.11';
@@ -118,7 +118,7 @@ export function createProjectWorker({ control, bridge, record, stateDir = connec
118
118
  requested_model: requestedModel, model: actualModel, last_turn_usage: lastUsage,
119
119
  permission_denials: { count: denialCount, tools: [...denialTools], last_at: lastDenialAt },
120
120
  unread_messages: Number.isSafeInteger(news?.pending) ? news.pending : null,
121
- scheduling, delivery: { pending: journal()?.data?.pending?.length ?? 0, notices: journal()?.data?.notices?.length ?? 0 },
121
+ scheduling, report_delivery: control.reportDeliveryStatus?.() ?? null, delivery: { pending: journal()?.data?.pending?.length ?? 0, notices: journal()?.data?.notices?.length ?? 0 },
122
122
  contact_known: typeof news?.project?.started === 'boolean' && typeof news?.project?.paused === 'boolean',
123
123
  work_available: news?.work?.runnable === true && Number.isSafeInteger(news.work.revision)
124
124
  && news.work.revision > (scheduling?.handledRevision ?? -1),
@@ -172,6 +172,7 @@ export function createProjectWorker({ control, bridge, record, stateDir = connec
172
172
  failureInfo = null; error = null;
173
173
  for (const item of applied) item.state = 'applied';
174
174
  failureStage = 'checkout_ownership';
175
+ control.assertReportCapacity?.();
175
176
  checkoutClaim.beginTurn(); invokedAt = now(); actualModel = null;
176
177
  failureStage = 'native_execution';
177
178
  notify();
@@ -184,8 +185,6 @@ export function createProjectWorker({ control, bridge, record, stateDir = connec
184
185
  })();
185
186
  try {
186
187
  const result = await turn;
187
- failureStage = 'checkout_cleanup';
188
- checkoutClaim.endTurn();
189
188
  let lastTurn;
190
189
  if (invokedAt !== undefined) {
191
190
  // Sequence belongs to the fenced service run, not this finite worker mission.
@@ -199,6 +198,9 @@ export function createProjectWorker({ control, bridge, record, stateDir = connec
199
198
  ...lastUsage, ...(lastUsage?.tokens ? { tokens: { input: null, output: null, cacheRead: null, cacheWrite: null, modelCalls: null, ...lastUsage.tokens } } : {}),
200
199
  sequence: usageSequence, seconds: Math.max(0, (endedAt - invokedAt) / 1000) } };
201
200
  }
201
+ if (lastTurn) { failureStage = 'completion_storage'; lastTurn = control.recordTurn?.(lastTurn) ?? lastTurn; }
202
+ failureStage = 'checkout_cleanup';
203
+ checkoutClaim.endTurn();
202
204
  if (result?.interrupted) { scheduler.recordOutcome({ kind: sendDeferred ? 'send_deferred' : 'interrupted', news: control.news, now: now() }); if (!stopRequested && phase !== 'paused') phase = 'waiting'; }
203
205
  else if (result?.failed) {
204
206
  if (result.failureCode !== 'runtime_contact_failure') {
@@ -220,7 +222,7 @@ export function createProjectWorker({ control, bridge, record, stateDir = connec
220
222
  try { await control.setPhase?.('idle', lastTurn); }
221
223
  catch (failure) {
222
224
  if (failure.fatal) throw failure;
223
- // Control keeps the report for its next phase/contact/close request. Retrying
225
+ // Control retains the durable queue for its next phase/contact/close request. Retrying
224
226
  // telemetry must neither rerun completed model work nor cancel a model turn.
225
227
  control.log?.('Worker turn report pending; runtime contact will retry.');
226
228
  }