@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 +3 -3
- package/package.json +1 -1
- package/runtime/control-v2.mjs +63 -5
- package/runtime/manifest.json +2 -1
- package/runtime/turn-report-journal.mjs +81 -0
- package/src/reusable-stdio.mjs +2 -1
- package/src/session-stdio.mjs +1 -1
- package/src/system.mjs +1 -1
- package/src/version.mjs +1 -1
- package/src/worker-runtime.mjs +6 -4
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Karma42 MCP connector
|
|
2
2
|
|
|
3
|
-
`0.1.0-beta.
|
|
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
|
+
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.
|
|
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
package/runtime/control-v2.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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.
|
|
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.
|
|
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;
|
package/runtime/manifest.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": "pilot/agent-kit",
|
|
3
3
|
"files": {
|
|
4
|
-
"control-v2.mjs": "
|
|
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
|
+
}
|
package/src/reusable-stdio.mjs
CHANGED
|
@@ -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
|
-
|
|
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.');
|
package/src/session-stdio.mjs
CHANGED
|
@@ -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.
|
|
1
|
+
export const VERSION = '0.1.0-beta.11';
|
package/src/worker-runtime.mjs
CHANGED
|
@@ -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
|
|
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
|
}
|