@commonlyai/cli 0.1.36 → 0.1.37
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/package.json +1 -1
- package/src/commands/agent.js +127 -6
- package/src/commands/daemon.js +33 -2
- package/src/lib/adapters/codex.js +6 -0
- package/src/lib/daemon-store.js +10 -0
- package/src/lib/daemon-supervisor.js +53 -11
- package/src/lib/poller.js +26 -0
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -18,7 +18,7 @@ import { fileURLToPath } from 'url';
|
|
|
18
18
|
|
|
19
19
|
import { createClient } from '../lib/api.js';
|
|
20
20
|
import { getToken, resolveInstanceUrl } from '../lib/config.js';
|
|
21
|
-
import { startPoller } from '../lib/poller.js';
|
|
21
|
+
import { startPoller, terminalDeliveryAckError } from '../lib/poller.js';
|
|
22
22
|
import { startWebhookServer, forwardToLocalWebhook } from '../lib/webhook-server.js';
|
|
23
23
|
import { getAdapter, listAdapterNames } from '../lib/adapters/index.js';
|
|
24
24
|
import {
|
|
@@ -32,7 +32,7 @@ import { readLongTerm, syncBack } from '../lib/memory-bridge.js';
|
|
|
32
32
|
import { pollRetryPolicy } from '../lib/poll-retry.js';
|
|
33
33
|
import { detectMemorySources, composeImport, importMemory } from '../lib/memory-import.js';
|
|
34
34
|
import { detectSkills, importSkills } from '../lib/skills-import.js';
|
|
35
|
-
import { parseEnvironmentFile, resolveWorkspace } from '../lib/environment.js';
|
|
35
|
+
import { parseEnvironmentFile, resolveWorkspace, validateEnvironmentSpec } from '../lib/environment.js';
|
|
36
36
|
import { detectBwrap } from '../lib/sandbox/bwrap.js';
|
|
37
37
|
import { detectSeatbelt } from '../lib/sandbox/seatbelt.js';
|
|
38
38
|
import {
|
|
@@ -376,6 +376,73 @@ export const setWakeOnMessage = async ({ client, record, enabled }) => {
|
|
|
376
376
|
return { agentName: record.agentName, podId: record.podId, instanceId, enabled: Boolean(enabled) };
|
|
377
377
|
};
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Update the server-side configuration for an existing local agent. The
|
|
381
|
+
* registry PATCH route already fans this change out to accessible pod peers;
|
|
382
|
+
* keeping the CLI on that route avoids a second config API and lets the daemon
|
|
383
|
+
* pick up the change on its next work-list pass.
|
|
384
|
+
*/
|
|
385
|
+
export const updateAgentConfiguration = async ({
|
|
386
|
+
client,
|
|
387
|
+
record,
|
|
388
|
+
model = null,
|
|
389
|
+
effort = null,
|
|
390
|
+
envPath = null,
|
|
391
|
+
parseEnv = parseEnvironmentFile,
|
|
392
|
+
}) => {
|
|
393
|
+
if (!record?.podId || !record?.agentName) {
|
|
394
|
+
throw new Error('token record is missing podId/agentName — re-attach the agent');
|
|
395
|
+
}
|
|
396
|
+
const instanceId = record.instanceId || 'default';
|
|
397
|
+
const runtime = {};
|
|
398
|
+
if (model !== null && model !== undefined) {
|
|
399
|
+
const normalizedModel = String(model);
|
|
400
|
+
const validation = validateEnvironmentSpec({ model: normalizedModel });
|
|
401
|
+
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
402
|
+
runtime.model = normalizedModel;
|
|
403
|
+
}
|
|
404
|
+
if (effort !== null && effort !== undefined) {
|
|
405
|
+
const normalizedEffort = String(effort);
|
|
406
|
+
const validation = validateEnvironmentSpec({ effort: normalizedEffort });
|
|
407
|
+
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
408
|
+
runtime.effort = normalizedEffort;
|
|
409
|
+
}
|
|
410
|
+
const config = {};
|
|
411
|
+
if (Object.keys(runtime).length) config.runtime = runtime;
|
|
412
|
+
let environment = null;
|
|
413
|
+
if (envPath) {
|
|
414
|
+
environment = await parseEnv(envPath);
|
|
415
|
+
} else if (record.environment && typeof record.environment === 'object' && !Array.isArray(record.environment)) {
|
|
416
|
+
environment = { ...record.environment };
|
|
417
|
+
}
|
|
418
|
+
// Keep the two historical control surfaces coherent. If a local token has
|
|
419
|
+
// an ADR-008 environment, a model/effort flag must update that declaration
|
|
420
|
+
// too; otherwise the daemon would correctly prefer the old explicit value
|
|
421
|
+
// over the new legacy runtime overlay.
|
|
422
|
+
if (environment && Object.keys(runtime).length) {
|
|
423
|
+
environment = { ...environment, ...runtime };
|
|
424
|
+
}
|
|
425
|
+
if (!environment && Object.keys(runtime).length) {
|
|
426
|
+
environment = { ...runtime };
|
|
427
|
+
}
|
|
428
|
+
if (environment) config.environment = environment;
|
|
429
|
+
if (!Object.keys(config).length) {
|
|
430
|
+
throw new Error('provide at least one of --model, --effort, or --env');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
await client.patch(
|
|
434
|
+
`/api/registry/pods/${record.podId}/agents/${record.agentName}`,
|
|
435
|
+
{ instanceId, config },
|
|
436
|
+
);
|
|
437
|
+
return {
|
|
438
|
+
agentName: record.agentName,
|
|
439
|
+
podId: record.podId,
|
|
440
|
+
instanceId,
|
|
441
|
+
changed: Object.keys(config),
|
|
442
|
+
...(environment ? { environment } : {}),
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
|
|
379
446
|
// ── attach: register a local-CLI-wrapped agent (ADR-005) ────────────────────
|
|
380
447
|
|
|
381
448
|
/**
|
|
@@ -1586,6 +1653,12 @@ export const performRun = ({
|
|
|
1586
1653
|
...(typeof deliveryId === 'string' && deliveryId ? { deliveryId } : {}),
|
|
1587
1654
|
});
|
|
1588
1655
|
} catch (ackErr) {
|
|
1656
|
+
const terminalError = terminalDeliveryAckError(ackErr, event._id, 'agent run');
|
|
1657
|
+
if (terminalError) {
|
|
1658
|
+
running = false;
|
|
1659
|
+
onError?.(terminalError);
|
|
1660
|
+
break;
|
|
1661
|
+
}
|
|
1589
1662
|
onError?.(new Error(`Ack failed for ${event._id}: ${ackErr.message}`));
|
|
1590
1663
|
}
|
|
1591
1664
|
}
|
|
@@ -1670,6 +1743,12 @@ export const performRun = ({
|
|
|
1670
1743
|
...(typeof deliveryId === 'string' && deliveryId ? { deliveryId } : {}),
|
|
1671
1744
|
});
|
|
1672
1745
|
} catch (ackErr) {
|
|
1746
|
+
const terminalError = terminalDeliveryAckError(ackErr, event._id, 'agent run');
|
|
1747
|
+
if (terminalError) {
|
|
1748
|
+
running = false;
|
|
1749
|
+
onError?.(terminalError);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1673
1752
|
onError?.(new Error(`Ack failed for ${event._id}: ${ackErr.message}`));
|
|
1674
1753
|
}
|
|
1675
1754
|
}
|
|
@@ -1946,6 +2025,7 @@ Examples:
|
|
|
1946
2025
|
|
|
1947
2026
|
# List installed agents
|
|
1948
2027
|
$ commonly agent list
|
|
2028
|
+
$ commonly agent config my-claude --model gpt-5.4 --effort high
|
|
1949
2029
|
|
|
1950
2030
|
Docs:
|
|
1951
2031
|
https://github.com/Team-Commonly/commonly/blob/main/docs/agents/LOCAL_CLI_WRAPPER.md
|
|
@@ -2357,11 +2437,15 @@ Docs:
|
|
|
2357
2437
|
onError: (err) => console.error(`${stamp()} [${name}] ${err.message}`),
|
|
2358
2438
|
});
|
|
2359
2439
|
|
|
2360
|
-
|
|
2361
|
-
|
|
2440
|
+
// SIGTERM is the daemon's idle-boundary handoff signal. `stop()` stops
|
|
2441
|
+
// future polls but lets the in-flight turn finish and ack before the
|
|
2442
|
+
// event loop drains; exiting here would cut a model turn in half.
|
|
2443
|
+
const requestStop = () => {
|
|
2444
|
+
console.log(`\n${stamp()} [${name}] stopping after the current turn...`);
|
|
2362
2445
|
stop();
|
|
2363
|
-
|
|
2364
|
-
|
|
2446
|
+
};
|
|
2447
|
+
process.on('SIGINT', requestStop);
|
|
2448
|
+
process.on('SIGTERM', requestStop);
|
|
2365
2449
|
});
|
|
2366
2450
|
|
|
2367
2451
|
// ── detach (ADR-005) ──────────────────────────────────────────────────────
|
|
@@ -2564,6 +2648,43 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
|
|
|
2564
2648
|
}
|
|
2565
2649
|
});
|
|
2566
2650
|
|
|
2651
|
+
// ── config (existing-agent edit) ─────────────────────────────────────────
|
|
2652
|
+
agent
|
|
2653
|
+
.command('config <name>')
|
|
2654
|
+
.description('Update an attached agent\'s server-side runtime configuration')
|
|
2655
|
+
.option('--model <id>', 'Model identifier to use on the next daemon restart')
|
|
2656
|
+
.option('--effort <level>', 'Reasoning effort (low|medium|high|xhigh|max)')
|
|
2657
|
+
.option('--env <path>', 'Replace the ADR-008 environment spec with this JSON file')
|
|
2658
|
+
.option('--instance <url>', 'Target Commonly instance')
|
|
2659
|
+
.action(async (name, opts) => {
|
|
2660
|
+
const record = loadAgentToken(name);
|
|
2661
|
+
if (!record) {
|
|
2662
|
+
console.error(`No token file for '${name}' — is it attached on this machine? (commonly agent list --local)`);
|
|
2663
|
+
process.exit(1);
|
|
2664
|
+
}
|
|
2665
|
+
const instance = opts.instance || record.instanceUrl;
|
|
2666
|
+
const token = getToken(instance);
|
|
2667
|
+
if (!token) { console.error('Not logged in. Run: commonly login'); process.exit(1); }
|
|
2668
|
+
const client = createClient({ instance: resolveInstanceUrl(instance), token });
|
|
2669
|
+
try {
|
|
2670
|
+
const result = await updateAgentConfiguration({
|
|
2671
|
+
client,
|
|
2672
|
+
record,
|
|
2673
|
+
model: opts.model,
|
|
2674
|
+
effort: opts.effort,
|
|
2675
|
+
envPath: opts.env ? pathResolve(opts.env) : null,
|
|
2676
|
+
});
|
|
2677
|
+
if (result.environment) {
|
|
2678
|
+
saveAgentToken(name, { ...record, environment: result.environment });
|
|
2679
|
+
}
|
|
2680
|
+
console.log(`✓ Updated ${result.agentName} in pod ${result.podId} (${result.changed.join(', ')})`);
|
|
2681
|
+
console.log(' The daemon will apply the change on its next poll; a standalone agent run needs a restart.');
|
|
2682
|
+
} catch (err) {
|
|
2683
|
+
console.error(`Failed: ${err.message}`);
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
|
|
2567
2688
|
// ── wake (ADR-018 ambient wake toggle) ───────────────────────────────────
|
|
2568
2689
|
agent
|
|
2569
2690
|
.command('wake <name> <on|off>')
|
package/src/commands/daemon.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { join } from 'path';
|
|
15
15
|
import { createClient } from '../lib/api.js';
|
|
16
16
|
import { getToken, resolveInstanceUrl } from '../lib/config.js';
|
|
17
|
-
import { loadDaemonRecord, saveDaemonRecord } from '../lib/daemon-store.js';
|
|
17
|
+
import { loadDaemonRecord, removeDaemonRecord, saveDaemonRecord } from '../lib/daemon-store.js';
|
|
18
18
|
import {
|
|
19
19
|
createDaemonSupervisor,
|
|
20
20
|
DEFAULT_HEARTBEAT_MS,
|
|
@@ -78,7 +78,7 @@ export const registerDaemonMachine = async ({
|
|
|
78
78
|
await client.del(`/api/machines/${machine.id}`);
|
|
79
79
|
} catch (revokeError) {
|
|
80
80
|
throw new Error(
|
|
81
|
-
`Could not store the daemon credential and could not revoke machine ${machine.name}: ${revokeError.message}`,
|
|
81
|
+
`Could not store the daemon credential and could not revoke machine ${machine.name} (${machine.id}): ${revokeError.message}`,
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
throw new Error(`Could not store the daemon credential securely: ${error.message}. Machine registration was revoked.`);
|
|
@@ -96,6 +96,18 @@ export const getDaemonMachineStatus = async ({ client }) => {
|
|
|
96
96
|
return response?.machine || null;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
+
// Revoke remotely before unlinking locally. A failed network call must leave
|
|
100
|
+
// the bearer record intact so the operator can retry rather than orphaning a
|
|
101
|
+
// year-long daemon credential they can no longer address from the CLI.
|
|
102
|
+
export const unregisterDaemonMachine = async ({ client, record, remove = removeDaemonRecord }) => {
|
|
103
|
+
try {
|
|
104
|
+
await client.del(`/api/machines/${record.machineDbId}`);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.status !== 404) throw error;
|
|
107
|
+
}
|
|
108
|
+
remove();
|
|
109
|
+
};
|
|
110
|
+
|
|
99
111
|
// The adapter names a binary on THIS machine — the one fact the server cannot
|
|
100
112
|
// know (same reasoning as `agent run`'s env bootstrap). A server-declared
|
|
101
113
|
// preference is honored when that CLI is installed; otherwise probe the known
|
|
@@ -121,6 +133,7 @@ Examples:
|
|
|
121
133
|
$ commonly daemon register --name "Sam's MacBook"
|
|
122
134
|
$ commonly daemon heartbeat
|
|
123
135
|
$ commonly daemon status
|
|
136
|
+
$ commonly daemon unregister
|
|
124
137
|
`);
|
|
125
138
|
|
|
126
139
|
daemon
|
|
@@ -160,6 +173,24 @@ Examples:
|
|
|
160
173
|
}
|
|
161
174
|
});
|
|
162
175
|
|
|
176
|
+
daemon
|
|
177
|
+
.command('unregister')
|
|
178
|
+
.description('Revoke this machine on the server and remove its local daemon credential')
|
|
179
|
+
.action(async () => {
|
|
180
|
+
try {
|
|
181
|
+
const record = requireDaemonRecord();
|
|
182
|
+
const client = createClient({
|
|
183
|
+
instance: record.instanceUrl,
|
|
184
|
+
token: requireUserToken(record.instanceUrl),
|
|
185
|
+
});
|
|
186
|
+
await unregisterDaemonMachine({ client, record });
|
|
187
|
+
console.log(`Unregistered ${record.machineName} and removed its local daemon credential.`);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(`Daemon unregister failed: ${error.message}`);
|
|
190
|
+
process.exitCode = 1;
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
163
194
|
daemon
|
|
164
195
|
.command('heartbeat')
|
|
165
196
|
.description('Send a machine liveness heartbeat using the stored daemon credential')
|
|
@@ -267,6 +267,8 @@ const buildArgs = ({
|
|
|
267
267
|
outputFile,
|
|
268
268
|
mcpFlags = [],
|
|
269
269
|
publicSandboxMode = null,
|
|
270
|
+
model = null,
|
|
271
|
+
effort = null,
|
|
270
272
|
}) => {
|
|
271
273
|
const publicSandbox = publicSandboxMode !== null;
|
|
272
274
|
// `--dangerously-bypass-approvals-and-sandbox` disables codex CLI's
|
|
@@ -293,6 +295,8 @@ const buildArgs = ({
|
|
|
293
295
|
'--json',
|
|
294
296
|
'--skip-git-repo-check',
|
|
295
297
|
...executionPolicy,
|
|
298
|
+
...(model ? ['--model', String(model)] : []),
|
|
299
|
+
...(effort ? ['-c', 'model_reasoning_effort=' + toml(effort)] : []),
|
|
296
300
|
...mcpFlags,
|
|
297
301
|
'-o',
|
|
298
302
|
outputFile,
|
|
@@ -444,6 +448,8 @@ export default {
|
|
|
444
448
|
outputFile,
|
|
445
449
|
mcpFlags: mcp.flags,
|
|
446
450
|
publicSandboxMode,
|
|
451
|
+
model: ctx.environment?.model,
|
|
452
|
+
effort: ctx.environment?.effort,
|
|
447
453
|
});
|
|
448
454
|
const childEnv = { ...(ctx.env || process.env), ...mcp.forwardedEnv };
|
|
449
455
|
if (publicSandboxMode !== null) {
|
package/src/lib/daemon-store.js
CHANGED
|
@@ -73,3 +73,13 @@ export const saveDaemonRecord = (record) => {
|
|
|
73
73
|
throw error;
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
|
+
|
|
77
|
+
// Call this only after the owner has revoked the matching server-side machine.
|
|
78
|
+
// Local deletion first would strand a live credential with no CLI path to
|
|
79
|
+
// revoke it, which is exactly the failure this command exists to prevent.
|
|
80
|
+
export const removeDaemonRecord = () => {
|
|
81
|
+
const path = daemonRecordPath();
|
|
82
|
+
if (!existsSync(path)) return false;
|
|
83
|
+
unlinkSync(path);
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { isAbsolute, resolve as pathResolve } from 'node:path';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* ADR-026 Phase 2, slice 2: the resident supervision loop behind
|
|
3
7
|
* `commonly daemon run`.
|
|
@@ -22,6 +26,15 @@ export const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
|
22
26
|
export const BACKOFF_BASE_MS = 5_000;
|
|
23
27
|
export const BACKOFF_MAX_MS = 60_000;
|
|
24
28
|
|
|
29
|
+
const workspacePathFor = (environment) => {
|
|
30
|
+
const declared = environment?.workspace?.path;
|
|
31
|
+
if (typeof declared !== 'string' || !declared.trim()) return null;
|
|
32
|
+
const expanded = declared === '~'
|
|
33
|
+
? homedir()
|
|
34
|
+
: (declared.startsWith('~/') ? `${homedir()}/${declared.slice(2)}` : declared);
|
|
35
|
+
return isAbsolute(expanded) ? expanded : pathResolve(expanded);
|
|
36
|
+
};
|
|
37
|
+
|
|
25
38
|
export const backoffMs = (restarts) => Math.min(
|
|
26
39
|
BACKOFF_MAX_MS,
|
|
27
40
|
BACKOFF_BASE_MS * 2 ** Math.max(0, Math.min(restarts, 10)),
|
|
@@ -87,11 +100,24 @@ export const createDaemonSupervisor = ({
|
|
|
87
100
|
}
|
|
88
101
|
};
|
|
89
102
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
103
|
+
// Preserve the complete ADR-008 environment when the daemon receives it.
|
|
104
|
+
// Older installs only expose runtime.model/effort; those fields are a
|
|
105
|
+
// compatibility overlay and must merge into an existing local environment
|
|
106
|
+
// rather than erasing its workspace, skills, or MCP declarations.
|
|
107
|
+
const environmentFor = (row) => {
|
|
108
|
+
const declared = row.environment && typeof row.environment === 'object'
|
|
109
|
+
&& !Array.isArray(row.environment) ? { ...row.environment } : null;
|
|
110
|
+
const runtime = row.runtime && typeof row.runtime === 'object' ? row.runtime : {};
|
|
111
|
+
if (declared) {
|
|
112
|
+
if (runtime.model && declared.model === undefined) declared.model = String(runtime.model);
|
|
113
|
+
if (runtime.effort && declared.effort === undefined) declared.effort = String(runtime.effort);
|
|
114
|
+
return { value: declared, declared: true };
|
|
115
|
+
}
|
|
116
|
+
const fallback = {};
|
|
117
|
+
if (runtime.model) fallback.model = String(runtime.model);
|
|
118
|
+
if (runtime.effort) fallback.effort = String(runtime.effort);
|
|
119
|
+
return Object.keys(fallback).length ? { value: fallback, declared: false } : null;
|
|
120
|
+
};
|
|
95
121
|
|
|
96
122
|
// Ensure ~/.commonly/tokens/<name>.json exists so `agent run` can boot.
|
|
97
123
|
// The mint refuses to clobber an existing token (409 token_exists); the
|
|
@@ -107,10 +133,22 @@ export const createDaemonSupervisor = ({
|
|
|
107
133
|
// once at boot). A row with NO declared model leaves the record alone —
|
|
108
134
|
// never strip an operator's hand-set environment.
|
|
109
135
|
const wanted = environmentFor(row);
|
|
110
|
-
if (wanted
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
if (wanted) {
|
|
137
|
+
const nextEnvironment = wanted.declared
|
|
138
|
+
? wanted.value
|
|
139
|
+
: { ...(existing.environment || {}), ...wanted.value };
|
|
140
|
+
const workspacePath = workspacePathFor(nextEnvironment);
|
|
141
|
+
const nextRecord = {
|
|
142
|
+
...existing,
|
|
143
|
+
environment: nextEnvironment,
|
|
144
|
+
...(workspacePath ? { workspacePath } : {}),
|
|
145
|
+
};
|
|
146
|
+
if (!isDeepStrictEqual(existing.environment || null, nextEnvironment)
|
|
147
|
+
|| (workspacePath && existing.workspacePath !== workspacePath)) {
|
|
148
|
+
saveToken(row.agentName, nextRecord);
|
|
149
|
+
log('runtime config changed — restarting the seat to load it');
|
|
150
|
+
return 'changed';
|
|
151
|
+
}
|
|
114
152
|
}
|
|
115
153
|
return 'ready';
|
|
116
154
|
}
|
|
@@ -149,9 +187,13 @@ export const createDaemonSupervisor = ({
|
|
|
149
187
|
instanceUrl: record.instanceUrl,
|
|
150
188
|
podId: row.podIds?.[0] || null,
|
|
151
189
|
adapter,
|
|
152
|
-
...(environment ? { environment } : {}),
|
|
190
|
+
...(environment ? { environment: environment.value } : {}),
|
|
191
|
+
...(environment?.value ? (() => {
|
|
192
|
+
const workspacePath = workspacePathFor(environment.value);
|
|
193
|
+
return workspacePath ? { workspacePath } : {};
|
|
194
|
+
})() : {}),
|
|
153
195
|
});
|
|
154
|
-
log(`[${row.agentName}] provisioned runtime token (adapter: ${adapter}${environment ? `, model: ${environment.model}` : ''})`);
|
|
196
|
+
log(`[${row.agentName}] provisioned runtime token (adapter: ${adapter}${environment?.value?.model ? `, model: ${environment.value.model}` : ''})`);
|
|
155
197
|
return 'ready';
|
|
156
198
|
};
|
|
157
199
|
|
package/src/lib/poller.js
CHANGED
|
@@ -8,6 +8,26 @@
|
|
|
8
8
|
|
|
9
9
|
import { createClient } from './api.js';
|
|
10
10
|
|
|
11
|
+
// Delivery failures that must stop a consumer rather than fall through to the
|
|
12
|
+
// normal at-least-once retry path. A stale delivery belongs to a replacement
|
|
13
|
+
// runner; a missing required nonce means this runner is misconfigured. Neither
|
|
14
|
+
// can become healthy by immediately fetching and retrying more work.
|
|
15
|
+
export const terminalDeliveryAckError = (ackErr, eventId, runner = 'poller') => {
|
|
16
|
+
if (ackErr?.status === 409 && ackErr?.body?.code === 'stale_delivery') {
|
|
17
|
+
return Object.assign(
|
|
18
|
+
new Error(`Delivery ${eventId} was superseded — stopping ${runner}.`),
|
|
19
|
+
{ code: 'stale_delivery' },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (ackErr?.status === 400 && ackErr?.body?.code === 'delivery_id_required') {
|
|
23
|
+
return Object.assign(
|
|
24
|
+
new Error(`Delivery ${eventId} requires deliveryId — stopping ${runner}; update its configuration.`),
|
|
25
|
+
{ code: 'delivery_id_required' },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
11
31
|
export const startPoller = ({
|
|
12
32
|
instanceUrl,
|
|
13
33
|
token,
|
|
@@ -53,6 +73,12 @@ export const startPoller = ({
|
|
|
53
73
|
...(typeof deliveryId === 'string' && deliveryId ? { deliveryId } : {}),
|
|
54
74
|
});
|
|
55
75
|
} catch (ackErr) {
|
|
76
|
+
const terminalError = terminalDeliveryAckError(ackErr, event._id);
|
|
77
|
+
if (terminalError) {
|
|
78
|
+
running = false;
|
|
79
|
+
onError?.(terminalError);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
56
82
|
// Non-fatal — event will be retried
|
|
57
83
|
onError?.(new Error(`Ack failed for ${event._id}: ${ackErr.message}`));
|
|
58
84
|
}
|