polygram 0.38.0 → 0.38.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/codex/fault-provenance.js +75 -0
- package/lib/codex/runtime-controller.js +4 -0
- package/lib/db/codex-failed-generation.js +8 -1
- package/lib/db/events-retention.js +1 -0
- package/lib/db.js +49 -0
- package/lib/handlers/dispatcher.js +13 -0
- package/lib/handlers/poll.js +138 -59
- package/lib/handlers/questions.js +26 -1
- package/lib/ipc/handlers.js +29 -0
- package/lib/ops/clean-restart-qualification.js +261 -0
- package/lib/ops/clean-resume.js +12 -1
- package/lib/ops/clean-shutdown.js +105 -6
- package/lib/ops/daemon-identity.js +42 -0
- package/lib/ops/deploy-restart-handler.js +111 -0
- package/lib/ops/foreground-canary-target.js +454 -0
- package/lib/ops/systemd-invocation.js +10 -3
- package/package.json +2 -2
- package/polygram.js +117 -47
- package/scripts/ipc-smoke.js +38 -3
package/polygram.js
CHANGED
|
@@ -32,13 +32,21 @@ const { buildPrompt, resolvePromptBackend } = require('./lib/prompt');
|
|
|
32
32
|
const { countInFlight } = require('./lib/queue-utils');
|
|
33
33
|
const { createIpcHandlers } = require('./lib/ipc/handlers');
|
|
34
34
|
const {
|
|
35
|
-
|
|
36
|
-
} = require('./lib/
|
|
35
|
+
buildQualificationEvent,
|
|
36
|
+
} = require('./lib/ops/clean-restart-qualification');
|
|
37
|
+
const {
|
|
38
|
+
createForegroundCanaryAuthorizer,
|
|
39
|
+
} = require('./lib/ops/foreground-canary-target');
|
|
40
|
+
const {
|
|
41
|
+
createDeployRestartHandler,
|
|
42
|
+
} = require('./lib/ops/deploy-restart-handler');
|
|
37
43
|
const { classifyOrphanSweep } = require('./lib/ops/tmux-preflight');
|
|
38
44
|
const {
|
|
39
45
|
parseSystemdInvocationId,
|
|
40
46
|
lifecycleDetail,
|
|
41
47
|
} = require('./lib/ops/systemd-invocation');
|
|
48
|
+
const { createDaemonIdentity } = require('./lib/ops/daemon-identity');
|
|
49
|
+
const packageMetadata = require('./package.json');
|
|
42
50
|
const { filterAttachments, resolveFileCaps, resolveMaxFileOverride, MAX_TOTAL_BYTES } = require('./lib/attachments');
|
|
43
51
|
// 0.9.0: SDK ProcessManager is the only pm. CLI pm
|
|
44
52
|
// (lib/process-manager.js) deleted in commit 6.
|
|
@@ -194,6 +202,7 @@ const { createCgroupOomObserver } = require('./lib/ops/cgroup-oom-observer');
|
|
|
194
202
|
const { persistShutdownDisposition } = require('./lib/ops/shutdown-disposition');
|
|
195
203
|
const {
|
|
196
204
|
prepareCleanRetirement,
|
|
205
|
+
settleCrashShutdown,
|
|
197
206
|
buildResumeIntents,
|
|
198
207
|
} = require('./lib/ops/clean-shutdown');
|
|
199
208
|
const {
|
|
@@ -1068,6 +1077,8 @@ let attemptAutoResume = null;
|
|
|
1068
1077
|
let errorReplyText = null;
|
|
1069
1078
|
let queueWarnThreshold = null;
|
|
1070
1079
|
let inFlightHandlers = null;
|
|
1080
|
+
let getActiveHandlerCount = null;
|
|
1081
|
+
let getActiveHandlerTargets = null;
|
|
1071
1082
|
let awaitHandlerSettlement = null;
|
|
1072
1083
|
let trackHandlerTask = null;
|
|
1073
1084
|
let recoverCodexRequest = null;
|
|
@@ -3539,10 +3550,16 @@ function createBot(token) {
|
|
|
3539
3550
|
// ─── Manual polling — extracted to lib/handlers/poll.js ────────────
|
|
3540
3551
|
let pollBot = null;
|
|
3541
3552
|
let startPollWatchdog = null;
|
|
3553
|
+
let stopPolling = null;
|
|
3554
|
+
let awaitPollSettlement = null;
|
|
3542
3555
|
|
|
3543
3556
|
// ─── Main ───────────────────────────────────────────────────────────
|
|
3544
3557
|
|
|
3545
3558
|
async function main() {
|
|
3559
|
+
const daemonIdentity = createDaemonIdentity({
|
|
3560
|
+
mainPath: __filename,
|
|
3561
|
+
packageVersion: packageMetadata.version,
|
|
3562
|
+
});
|
|
3546
3563
|
const cgroupOomObserver = createCgroupOomObserver();
|
|
3547
3564
|
const invocationId = parseSystemdInvocationId();
|
|
3548
3565
|
let abnormalProviderTermination = null;
|
|
@@ -3787,7 +3804,7 @@ async function main() {
|
|
|
3787
3804
|
db.logEvent('polygram-start', lifecycleDetail({
|
|
3788
3805
|
migration: migration.reason,
|
|
3789
3806
|
imported: migration.imported,
|
|
3790
|
-
}, invocationId));
|
|
3807
|
+
}, invocationId, daemonIdentity));
|
|
3791
3808
|
if (cgroupOomObserver.startup.status === 'unavailable') {
|
|
3792
3809
|
console.warn(`[oom-observer] unavailable (${cgroupOomObserver.startup.reason}); handled shutdown signals will retain clean-restart behavior`);
|
|
3793
3810
|
db.logEvent('oom-observer-unavailable', { reason: cgroupOomObserver.startup.reason });
|
|
@@ -4410,6 +4427,8 @@ async function main() {
|
|
|
4410
4427
|
errorReplyText,
|
|
4411
4428
|
queueWarnThreshold,
|
|
4412
4429
|
inFlightHandlers,
|
|
4430
|
+
getActiveHandlerCount,
|
|
4431
|
+
getActiveHandlerTargets,
|
|
4413
4432
|
awaitSettlement: awaitHandlerSettlement,
|
|
4414
4433
|
trackTask: trackHandlerTask,
|
|
4415
4434
|
} = createDispatcher({
|
|
@@ -4483,7 +4502,7 @@ async function main() {
|
|
|
4483
4502
|
dropRedeliverer = createDropRedeliverer({
|
|
4484
4503
|
db, redeliver: redeliverAsFreshTurn, logEvent, logger: console,
|
|
4485
4504
|
});
|
|
4486
|
-
({ pollBot, startPollWatchdog } = createPollLoop({
|
|
4505
|
+
({ pollBot, startPollWatchdog, stopPolling, awaitPollSettlement } = createPollLoop({
|
|
4487
4506
|
db, dbWrite, config, botName: BOT_NAME,
|
|
4488
4507
|
isWellFormedMessage, getTopicName,
|
|
4489
4508
|
logger: console,
|
|
@@ -4533,6 +4552,7 @@ async function main() {
|
|
|
4533
4552
|
continuationAuthorized = false,
|
|
4534
4553
|
trigger = 'signal',
|
|
4535
4554
|
restartRequestId = null,
|
|
4555
|
+
qualificationExpectation,
|
|
4536
4556
|
} = {}) => {
|
|
4537
4557
|
if (isShuttingDown) return;
|
|
4538
4558
|
const shutdownObservation = cgroupOomObserver.sample();
|
|
@@ -4545,22 +4565,30 @@ async function main() {
|
|
|
4545
4565
|
isShuttingDown = true;
|
|
4546
4566
|
console.log(`\nShutting down (${trigger})...`);
|
|
4547
4567
|
// 1. Stop accepting new inbound first so nothing new queues behind the drain.
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
//
|
|
4554
|
-
//
|
|
4555
|
-
//
|
|
4568
|
+
stopPolling();
|
|
4569
|
+
const awaitIngressSettlement = ({ timeoutMs } = {}) => (
|
|
4570
|
+
awaitPollSettlement({ timeoutMs })
|
|
4571
|
+
);
|
|
4572
|
+
|
|
4573
|
+
// Resolve open questions before provider settlement. Authorized deploys
|
|
4574
|
+
// register the card edit and cancel the row without answering the retiring
|
|
4575
|
+
// provider; ordinary stops keep the existing answer-and-await behavior.
|
|
4556
4576
|
try {
|
|
4557
4577
|
const openQuestions = questionStore.listOpen?.(BOT_NAME) || [];
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
}
|
|
4578
|
+
if (continuationAuthorized) {
|
|
4579
|
+
for (const row of openQuestions) {
|
|
4580
|
+
questionHandlers.beginShutdownDisposition(row, {
|
|
4581
|
+
message: 'Bot is restarting — this question was cancelled. It may be re-asked in a moment.',
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
} else {
|
|
4585
|
+
for (const row of openQuestions) {
|
|
4586
|
+
// eslint-disable-next-line no-await-in-loop
|
|
4587
|
+
await questionHandlers.expireQuestion(row, {
|
|
4588
|
+
status: 'cancelled',
|
|
4589
|
+
message: 'Bot is restarting — this question was cancelled. It may be re-asked in a moment.',
|
|
4590
|
+
}).catch(() => {});
|
|
4591
|
+
}
|
|
4564
4592
|
}
|
|
4565
4593
|
if (openQuestions.length) {
|
|
4566
4594
|
logEvent('shutdown-questions-cancelled', { count: openQuestions.length });
|
|
@@ -4619,13 +4647,15 @@ async function main() {
|
|
|
4619
4647
|
// 2. Drain in-flight handlers. Wait for inFlightHandlers to empty or
|
|
4620
4648
|
// SHUTDOWN_DRAIN_MS to elapse. pm handlers resolve naturally when
|
|
4621
4649
|
// result events arrive; the dispatcher's .finally decrements.
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4650
|
+
let drainElapsed = 0;
|
|
4651
|
+
if (!continuationAuthorized) {
|
|
4652
|
+
const drainStart = Date.now();
|
|
4653
|
+
while (inFlightHandlers.size > 0) {
|
|
4654
|
+
if (Date.now() - drainStart >= SHUTDOWN_DRAIN_MS) break;
|
|
4655
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
4656
|
+
}
|
|
4657
|
+
drainElapsed = Date.now() - drainStart;
|
|
4626
4658
|
}
|
|
4627
|
-
const drainElapsed = Date.now() - drainStart;
|
|
4628
|
-
const remaining = countInFlight(inFlightHandlers);
|
|
4629
4659
|
|
|
4630
4660
|
// 3. A clean stop closes both output boundaries before persisting any
|
|
4631
4661
|
// recovery intent. The ProcessManager receipt is the only authority for
|
|
@@ -4636,14 +4666,38 @@ async function main() {
|
|
|
4636
4666
|
? 'provider-process-terminated'
|
|
4637
4667
|
: null;
|
|
4638
4668
|
let resumeIntents = [];
|
|
4639
|
-
if (shutdownObservation?.detected
|
|
4669
|
+
if (shutdownObservation?.detected === true) {
|
|
4670
|
+
try {
|
|
4671
|
+
await settleCrashShutdown({
|
|
4672
|
+
pm,
|
|
4673
|
+
deliveryBarrier,
|
|
4674
|
+
awaitIngressSettlement,
|
|
4675
|
+
awaitHandlerSettlement,
|
|
4676
|
+
settlementTimeoutMs: SHUTDOWN_DRAIN_MS,
|
|
4677
|
+
});
|
|
4678
|
+
} catch (err) {
|
|
4679
|
+
console.error(`[shutdown] crash settlement failed: ${err.message}`);
|
|
4680
|
+
}
|
|
4681
|
+
pmRetired = true;
|
|
4682
|
+
} else {
|
|
4640
4683
|
try {
|
|
4641
4684
|
const retirement = await prepareCleanRetirement({
|
|
4642
4685
|
pm,
|
|
4643
4686
|
deliveryBarrier,
|
|
4687
|
+
awaitIngressSettlement,
|
|
4644
4688
|
awaitHandlerSettlement,
|
|
4645
4689
|
settlementTimeoutMs: SHUTDOWN_DRAIN_MS,
|
|
4690
|
+
qualificationExpectation,
|
|
4646
4691
|
});
|
|
4692
|
+
if (qualificationExpectation !== undefined) {
|
|
4693
|
+
logEvent('clean-restart-qualification-observed', buildQualificationEvent({
|
|
4694
|
+
botName: BOT_NAME,
|
|
4695
|
+
daemonIdentity,
|
|
4696
|
+
restartRequestId,
|
|
4697
|
+
expectation: qualificationExpectation,
|
|
4698
|
+
observation: retirement.qualification,
|
|
4699
|
+
}));
|
|
4700
|
+
}
|
|
4647
4701
|
pmRetired = true;
|
|
4648
4702
|
const projected = continuationAuthorized ? buildResumeIntents({
|
|
4649
4703
|
snapshots: retirement.snapshots,
|
|
@@ -4686,12 +4740,16 @@ async function main() {
|
|
|
4686
4740
|
});
|
|
4687
4741
|
}
|
|
4688
4742
|
} catch (err) {
|
|
4689
|
-
forceCrashReason = '
|
|
4743
|
+
forceCrashReason = err?.code === 'POLL_OFFSET_PERSISTENCE_FAILED'
|
|
4744
|
+
? 'poll-offset-persistence-failed'
|
|
4745
|
+
: err?.code === 'POLL_SETTLEMENT_TIMEOUT'
|
|
4746
|
+
? 'poll-settlement-timeout'
|
|
4747
|
+
: 'clean-retirement-failed';
|
|
4690
4748
|
console.error(`[shutdown] clean retirement failed: ${err.message}`);
|
|
4691
|
-
if (pm) await pm.shutdown().catch(() => {});
|
|
4692
4749
|
pmRetired = true;
|
|
4693
4750
|
}
|
|
4694
4751
|
}
|
|
4752
|
+
const remaining = countInFlight(inFlightHandlers);
|
|
4695
4753
|
|
|
4696
4754
|
// 4. Persist only after the old delivery/process boundary is closed and
|
|
4697
4755
|
// every dispatcher-owned continuation has settled. Any uncertainty is
|
|
@@ -4732,7 +4790,7 @@ async function main() {
|
|
|
4732
4790
|
? restartRequestId
|
|
4733
4791
|
: null,
|
|
4734
4792
|
...(res.oomKillDelta != null ? { oom_kill_delta: res.oomKillDelta } : {}),
|
|
4735
|
-
}, invocationId));
|
|
4793
|
+
}, invocationId, daemonIdentity));
|
|
4736
4794
|
console.log(`[shutdown] ${res.clean ? 'clean' : 'crash-like'} shutdown recorded (${res.shutdownReason}); ${inFlightAtSignal} in-flight at signal, drained ${drainElapsed}ms, ${remaining} still in-flight, ${res.replayMarked} marked replay-pending`);
|
|
4737
4795
|
} catch (err) {
|
|
4738
4796
|
console.error(`[shutdown] persistence failed: ${err.message}`);
|
|
@@ -4766,7 +4824,7 @@ async function main() {
|
|
|
4766
4824
|
if (pm && !pmRetired) await pm.shutdown().catch(() => {});
|
|
4767
4825
|
if (db) {
|
|
4768
4826
|
try {
|
|
4769
|
-
db.logEvent('polygram-stop', lifecycleDetail({}, invocationId));
|
|
4827
|
+
db.logEvent('polygram-stop', lifecycleDetail({}, invocationId, daemonIdentity));
|
|
4770
4828
|
db.raw.close();
|
|
4771
4829
|
} catch {}
|
|
4772
4830
|
}
|
|
@@ -4797,30 +4855,42 @@ async function main() {
|
|
|
4797
4855
|
// scripts, hook); also exported to spawned Claude processes via env.
|
|
4798
4856
|
const ipcSecret = ipcServer.writeSecret(BOT_NAME);
|
|
4799
4857
|
process.env.POLYGRAM_IPC_SECRET = ipcSecret;
|
|
4858
|
+
const foregroundCanaryAuthorizer = createForegroundCanaryAuthorizer({
|
|
4859
|
+
botName: BOT_NAME,
|
|
4860
|
+
daemonIdentity,
|
|
4861
|
+
config,
|
|
4862
|
+
tokenSecret: ipcSecret,
|
|
4863
|
+
getActiveHandlerCount: () => getActiveHandlerCount(),
|
|
4864
|
+
getActiveHandlerTargets: () => getActiveHandlerTargets(),
|
|
4865
|
+
getForegroundCanaryTarget: (input) => (
|
|
4866
|
+
db.getForegroundCanaryTarget(input)
|
|
4867
|
+
),
|
|
4868
|
+
});
|
|
4869
|
+
const requestDeployRestart = createDeployRestartHandler({
|
|
4870
|
+
getIsShuttingDown: () => isShuttingDown,
|
|
4871
|
+
getPid: () => process.pid,
|
|
4872
|
+
foregroundCanaryAuthorizer,
|
|
4873
|
+
persistForegroundCanaryAuthorization: (detail) => (
|
|
4874
|
+
db.logEvent('foreground-canary-target-authorized', detail)
|
|
4875
|
+
),
|
|
4876
|
+
shutdown,
|
|
4877
|
+
logger: console,
|
|
4878
|
+
});
|
|
4800
4879
|
ipcCloser = await ipcServer.start({
|
|
4801
4880
|
path: ipcServer.socketPathFor(BOT_NAME),
|
|
4802
4881
|
secret: ipcSecret,
|
|
4803
4882
|
handlers: createIpcHandlers({
|
|
4804
4883
|
botName: BOT_NAME,
|
|
4884
|
+
daemonIdentity,
|
|
4805
4885
|
getInFlightHandlers: () => inFlightHandlers,
|
|
4806
4886
|
handleSendOverIpc: (req) => handleSendOverIpc(req),
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
const oldPid = process.pid;
|
|
4815
|
-
shutdown({
|
|
4816
|
-
continuationAuthorized: true,
|
|
4817
|
-
trigger: 'deploy-ipc',
|
|
4818
|
-
restartRequestId,
|
|
4819
|
-
}).catch((error) => {
|
|
4820
|
-
console.error(`[shutdown] deploy restart failed: ${error.message}`);
|
|
4821
|
-
});
|
|
4822
|
-
return { accepted: true, old_pid: oldPid };
|
|
4823
|
-
},
|
|
4887
|
+
requestForegroundCanaryTarget: (req) => (
|
|
4888
|
+
foregroundCanaryAuthorizer.probe(req)
|
|
4889
|
+
),
|
|
4890
|
+
inspectCleanRestartQualification: () => (
|
|
4891
|
+
pm.inspectCleanRestartQualification(undefined)
|
|
4892
|
+
),
|
|
4893
|
+
requestDeployRestart,
|
|
4824
4894
|
}),
|
|
4825
4895
|
logger: console,
|
|
4826
4896
|
});
|
|
@@ -5337,7 +5407,7 @@ async function main() {
|
|
|
5337
5407
|
|
|
5338
5408
|
logEvent('polygram-admission-open', lifecycleDetail({
|
|
5339
5409
|
bot: BOT_NAME,
|
|
5340
|
-
}, invocationId));
|
|
5410
|
+
}, invocationId, daemonIdentity));
|
|
5341
5411
|
console.log(`[${BOT_NAME}] Starting...`);
|
|
5342
5412
|
const pollPromise = pollBot(bot).catch(err => {
|
|
5343
5413
|
console.error(`[${BOT_NAME}] Fatal:`, err.message);
|
package/scripts/ipc-smoke.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Quick IPC round-trip probe.
|
|
4
|
-
* Usage: node scripts/ipc-smoke.js <bot-name> [busy|restart] [restart-request-id]
|
|
4
|
+
* Usage: node scripts/ipc-smoke.js <bot-name> [busy|identity|restart] [restart-request-id]
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const crypto = require('node:crypto');
|
|
@@ -14,6 +14,9 @@ function restartRequestId(value) {
|
|
|
14
14
|
return requireRestartRequestId(value || crypto.randomUUID());
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
18
|
+
const SHA256_RE = /^[0-9a-f]{64}$/;
|
|
19
|
+
|
|
17
20
|
(async () => {
|
|
18
21
|
const bot = process.argv[2] || 'shumabit';
|
|
19
22
|
const command = process.argv[3] || 'ping';
|
|
@@ -67,12 +70,44 @@ function restartRequestId(value) {
|
|
|
67
70
|
return;
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
if (command === 'identity') {
|
|
74
|
+
const result = await call({
|
|
75
|
+
path,
|
|
76
|
+
op: 'identity',
|
|
77
|
+
secret: readSecret(bot),
|
|
78
|
+
connectTimeoutMs: 5_000,
|
|
79
|
+
callTimeoutMs: 5_000,
|
|
80
|
+
});
|
|
81
|
+
if (
|
|
82
|
+
result.ok !== true
|
|
83
|
+
|| result.bot !== bot
|
|
84
|
+
|| !Number.isSafeInteger(result.pid)
|
|
85
|
+
|| result.pid <= 1
|
|
86
|
+
|| !UUID_RE.test(result.daemon_instance_id)
|
|
87
|
+
|| typeof result.package_version !== 'string'
|
|
88
|
+
|| result.package_version.length === 0
|
|
89
|
+
|| result.package_version.length > 128
|
|
90
|
+
|| !SHA256_RE.test(result.main_realpath_sha256)
|
|
91
|
+
) {
|
|
92
|
+
throw new Error('invalid daemon identity response');
|
|
93
|
+
}
|
|
94
|
+
console.log(JSON.stringify({
|
|
95
|
+
bot: result.bot,
|
|
96
|
+
pid: result.pid,
|
|
97
|
+
daemon_instance_id: result.daemon_instance_id,
|
|
98
|
+
package_version: result.package_version,
|
|
99
|
+
main_realpath_sha256: result.main_realpath_sha256,
|
|
100
|
+
}));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
70
104
|
console.log('path:', path);
|
|
71
105
|
console.log('ping:', JSON.stringify(await call({ path, op: 'ping' })));
|
|
72
106
|
console.log('DONE');
|
|
73
107
|
})().catch((err) => {
|
|
74
|
-
const
|
|
75
|
-
|
|
108
|
+
const command = process.argv[3];
|
|
109
|
+
const message = command === 'busy' || command === 'identity'
|
|
110
|
+
? `${command} request failed`
|
|
76
111
|
: err.message;
|
|
77
112
|
console.error('ERR:', message);
|
|
78
113
|
process.exit(1);
|