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.
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+
5
+ const SHA256_RE = /^[a-f0-9]{64}$/;
6
+ const PROCESS_STATES = new Set([
7
+ 'Spawning', 'Initializing', 'AttachingThread', 'Idle', 'StartingTurn',
8
+ 'Active', 'BackgroundWorking', 'BackgroundSettling', 'Settling',
9
+ 'Quiescing', 'Stopped', 'Closing', 'Closed', 'FailedAmbiguous',
10
+ 'RecoveryConflict', 'DurabilityBlocked', 'ContainmentFailed', 'unknown',
11
+ ]);
12
+ const OUTCOME_CODES = new Set([
13
+ 'eligible', 'active-turn', 'pending-delivery', 'background-owner',
14
+ 'background-terminals', 'pagination-incomplete', 'process-not-idle',
15
+ 'process-not-ready', 'no-codex-generation', 'unsupported', 'lease-drift',
16
+ 'generation-drift', 'activity-epoch-drift', 'fence-drift',
17
+ 'inspection-failed', 'invalid-expectation', 'incoherent',
18
+ ]);
19
+ const OBSERVATION_KEYS = [
20
+ 'outcome',
21
+ 'reason',
22
+ 'generationDigest',
23
+ 'activityEpoch',
24
+ 'processState',
25
+ 'activeTurnCount',
26
+ 'pendingDeliveryCount',
27
+ 'backgroundOwnerCount',
28
+ 'backgroundTerminalCount',
29
+ 'backgroundTerminalRegistryComplete',
30
+ 'observedAtMs',
31
+ ];
32
+
33
+ function isPlainObject(value) {
34
+ return value != null
35
+ && typeof value === 'object'
36
+ && !Array.isArray(value)
37
+ && Object.getPrototypeOf(value) === Object.prototype;
38
+ }
39
+
40
+ function hasExactKeys(value, expectedKeys) {
41
+ if (!isPlainObject(value)) return false;
42
+ const keys = Object.keys(value);
43
+ return keys.length === expectedKeys.length
44
+ && expectedKeys.every((key) => Object.hasOwn(value, key));
45
+ }
46
+
47
+ function validOptionalFence(value, validator) {
48
+ return value === null || validator(value);
49
+ }
50
+
51
+ function fallbackObservation(now = Date.now) {
52
+ return Object.freeze({
53
+ outcome: 'mismatch',
54
+ reason: 'inspection-failed',
55
+ generationDigest: null,
56
+ activityEpoch: null,
57
+ processState: 'unknown',
58
+ activeTurnCount: 0,
59
+ pendingDeliveryCount: 0,
60
+ backgroundOwnerCount: 0,
61
+ backgroundTerminalCount: 0,
62
+ backgroundTerminalRegistryComplete: false,
63
+ observedAtMs: now(),
64
+ });
65
+ }
66
+
67
+ function normalizeQualificationObservation(observation, { now = Date.now } = {}) {
68
+ if (
69
+ !hasExactKeys(observation, OBSERVATION_KEYS)
70
+ || !['qualified', 'mismatch'].includes(observation.outcome)
71
+ || !OUTCOME_CODES.has(observation.reason)
72
+ || !validOptionalFence(
73
+ observation.generationDigest,
74
+ (value) => SHA256_RE.test(value),
75
+ )
76
+ || !validOptionalFence(
77
+ observation.activityEpoch,
78
+ (value) => Number.isSafeInteger(value) && value >= 0,
79
+ )
80
+ || !PROCESS_STATES.has(observation.processState)
81
+ || ![0, 1].includes(observation.activeTurnCount)
82
+ || ![0, 1].includes(observation.pendingDeliveryCount)
83
+ || ![0, 1].includes(observation.backgroundOwnerCount)
84
+ || !Number.isSafeInteger(observation.backgroundTerminalCount)
85
+ || observation.backgroundTerminalCount < 0
86
+ || observation.backgroundTerminalCount > 1_600
87
+ || typeof observation.backgroundTerminalRegistryComplete !== 'boolean'
88
+ || !Number.isSafeInteger(observation.observedAtMs)
89
+ || observation.observedAtMs < 0
90
+ ) {
91
+ return fallbackObservation(now);
92
+ }
93
+
94
+ const qualified = observation.outcome === 'qualified';
95
+ const eligibleShape = (
96
+ observation.reason === 'eligible'
97
+ && SHA256_RE.test(observation.generationDigest)
98
+ && Number.isSafeInteger(observation.activityEpoch)
99
+ && observation.processState === 'Idle'
100
+ && observation.activeTurnCount === 0
101
+ && observation.pendingDeliveryCount === 0
102
+ && observation.backgroundOwnerCount === 0
103
+ && observation.backgroundTerminalCount === 0
104
+ && observation.backgroundTerminalRegistryComplete === true
105
+ );
106
+ if (
107
+ (qualified && !eligibleShape)
108
+ || (!qualified && observation.reason === 'eligible')
109
+ ) {
110
+ return fallbackObservation(now);
111
+ }
112
+
113
+ return Object.freeze({
114
+ outcome: observation.outcome,
115
+ reason: observation.reason,
116
+ generationDigest: observation.generationDigest,
117
+ activityEpoch: observation.activityEpoch,
118
+ processState: observation.processState,
119
+ activeTurnCount: observation.activeTurnCount,
120
+ pendingDeliveryCount: observation.pendingDeliveryCount,
121
+ backgroundOwnerCount: observation.backgroundOwnerCount,
122
+ backgroundTerminalCount: observation.backgroundTerminalCount,
123
+ backgroundTerminalRegistryComplete: observation.backgroundTerminalRegistryComplete,
124
+ observedAtMs: observation.observedAtMs,
125
+ });
126
+ }
127
+
128
+ function normalizeDeployQualificationExpectation(request) {
129
+ if (!isPlainObject(request)) return null;
130
+ const requestKeys = Object.keys(request);
131
+ const allowedRequestKeys = new Set([
132
+ 'op', 'id', 'secret', 'qualification_expectation',
133
+ 'foreground_expectation',
134
+ ]);
135
+ const hasUnknownRequestField = requestKeys.some(
136
+ (key) => !allowedRequestKeys.has(key),
137
+ );
138
+ const hasExpectation = Object.hasOwn(request, 'qualification_expectation');
139
+ if (!hasExpectation && !hasUnknownRequestField) return undefined;
140
+ if (hasUnknownRequestField) return null;
141
+
142
+ const expectation = request.qualification_expectation;
143
+ if (!hasExactKeys(expectation, ['generation_digest', 'activity_epoch'])) {
144
+ return null;
145
+ }
146
+ if (
147
+ !SHA256_RE.test(expectation.generation_digest)
148
+ || !Number.isSafeInteger(expectation.activity_epoch)
149
+ || expectation.activity_epoch < 0
150
+ ) {
151
+ return null;
152
+ }
153
+ return Object.freeze({
154
+ expectedGenerationDigest: expectation.generation_digest,
155
+ expectedActivityEpoch: expectation.activity_epoch,
156
+ });
157
+ }
158
+
159
+ function projectQualificationResponse({
160
+ botName,
161
+ daemonIdentity,
162
+ observation,
163
+ now = Date.now,
164
+ } = {}) {
165
+ const normalized = normalizeQualificationObservation(observation, { now });
166
+ return Object.freeze({
167
+ bot: botName,
168
+ daemon_instance_id: daemonIdentity?.daemon_instance_id ?? null,
169
+ package_version: daemonIdentity?.package_version ?? null,
170
+ observed_at_ms: normalized.observedAtMs,
171
+ generation_digest: normalized.generationDigest,
172
+ activity_epoch: normalized.activityEpoch,
173
+ process_state: normalized.processState,
174
+ active_turn_count: normalized.activeTurnCount,
175
+ pending_delivery_count: normalized.pendingDeliveryCount,
176
+ background_owner_count: normalized.backgroundOwnerCount,
177
+ background_terminal_count: normalized.backgroundTerminalCount,
178
+ background_terminal_registry_complete: normalized.backgroundTerminalRegistryComplete,
179
+ outcome_code: normalized.reason,
180
+ });
181
+ }
182
+
183
+ function buildQualificationEvent({
184
+ botName,
185
+ daemonIdentity,
186
+ restartRequestId,
187
+ expectation,
188
+ observation,
189
+ now = Date.now,
190
+ } = {}) {
191
+ const normalized = normalizeQualificationObservation(observation, { now });
192
+ const expectationValid = (
193
+ isPlainObject(expectation)
194
+ && hasExactKeys(expectation, [
195
+ 'expectedGenerationDigest',
196
+ 'expectedActivityEpoch',
197
+ ])
198
+ && SHA256_RE.test(expectation.expectedGenerationDigest)
199
+ && Number.isSafeInteger(expectation.expectedActivityEpoch)
200
+ && expectation.expectedActivityEpoch >= 0
201
+ );
202
+ const exactMatch = Boolean(
203
+ expectationValid
204
+ && normalized.outcome === 'qualified'
205
+ && normalized.reason === 'eligible'
206
+ && normalized.generationDigest === expectation.expectedGenerationDigest
207
+ && normalized.activityEpoch === expectation.expectedActivityEpoch
208
+ && normalized.processState === 'Idle'
209
+ && normalized.activeTurnCount === 0
210
+ && normalized.pendingDeliveryCount === 0
211
+ && normalized.backgroundOwnerCount === 0
212
+ && normalized.backgroundTerminalCount === 0
213
+ && normalized.backgroundTerminalRegistryComplete === true
214
+ );
215
+ return Object.freeze({
216
+ bot: botName,
217
+ restart_request_sha256: createHash('sha256')
218
+ .update(restartRequestId)
219
+ .digest('hex'),
220
+ daemon_instance_id: daemonIdentity?.daemon_instance_id ?? null,
221
+ package_version: daemonIdentity?.package_version ?? null,
222
+ observed_at_ms: normalized.observedAtMs,
223
+ generation_digest: normalized.generationDigest,
224
+ expected_activity_epoch: expectationValid
225
+ ? expectation.expectedActivityEpoch
226
+ : null,
227
+ observed_activity_epoch: normalized.activityEpoch,
228
+ process_state: normalized.processState,
229
+ active_turn_count: normalized.activeTurnCount,
230
+ pending_delivery_count: normalized.pendingDeliveryCount,
231
+ background_owner_count: normalized.backgroundOwnerCount,
232
+ background_terminal_count: normalized.backgroundTerminalCount,
233
+ background_terminal_registry_complete: normalized.backgroundTerminalRegistryComplete,
234
+ exact_match: exactMatch,
235
+ outcome_code: normalized.reason,
236
+ });
237
+ }
238
+
239
+ function assertClosedQualificationRequest(request) {
240
+ if (!isPlainObject(request)) {
241
+ throw new TypeError('clean restart qualification request is invalid');
242
+ }
243
+ const allowed = new Set(['op', 'id', 'secret']);
244
+ if (
245
+ request.op !== 'clean_restart_qualification'
246
+ || Object.keys(request).some((key) => !allowed.has(key))
247
+ ) {
248
+ const error = new Error('clean restart qualification request has unknown fields');
249
+ error.code = 'INVALID_CLEAN_RESTART_QUALIFICATION_REQUEST';
250
+ throw error;
251
+ }
252
+ return request;
253
+ }
254
+
255
+ module.exports = {
256
+ assertClosedQualificationRequest,
257
+ buildQualificationEvent,
258
+ normalizeDeployQualificationExpectation,
259
+ normalizeQualificationObservation,
260
+ projectQualificationResponse,
261
+ };
@@ -147,6 +147,17 @@ async function executeCleanResumeClaim(claim, {
147
147
  policy_version: claim?.policy_version ?? null,
148
148
  };
149
149
  const codex = isCodexClaim(claim);
150
+ let continuationDispatched = false;
151
+ const markContinuationDispatched = () => {
152
+ if (continuationDispatched) return;
153
+ continuationDispatched = true;
154
+ logEvent('clean-resume-continuation-dispatched', {
155
+ ...eventDetail,
156
+ provider: codex ? 'codex' : 'claude',
157
+ command_kind: 'continue',
158
+ });
159
+ markReady();
160
+ };
150
161
  const source = typeof loadSource === 'function'
151
162
  ? loadSource(claim.source_message_id)
152
163
  : null;
@@ -295,7 +306,7 @@ async function executeCleanResumeClaim(claim, {
295
306
  text: 'continue',
296
307
  sourceMsgId: codex ? null : source.msg_id,
297
308
  expectedProcess: spawned.process,
298
- onDispatched: markReady,
309
+ onDispatched: markContinuationDispatched,
299
310
  });
300
311
  markReady();
301
312
  } catch (error) {
@@ -3,8 +3,10 @@
3
3
  async function prepareCleanRetirement({
4
4
  pm,
5
5
  deliveryBarrier,
6
+ awaitIngressSettlement,
6
7
  awaitHandlerSettlement,
7
8
  settlementTimeoutMs = 30_000,
9
+ qualificationExpectation,
8
10
  } = {}) {
9
11
  if (typeof deliveryBarrier?.fenceAndDrain !== 'function'
10
12
  || typeof deliveryBarrier?.inspect !== 'function') {
@@ -13,22 +15,118 @@ async function prepareCleanRetirement({
13
15
  if (typeof pm?.retireForCleanRestart !== 'function') {
14
16
  throw new TypeError('ProcessManager.retireForCleanRestart is required');
15
17
  }
18
+ if (typeof awaitIngressSettlement !== 'function') {
19
+ throw new TypeError('clean shutdown requires ingress settlement');
20
+ }
16
21
  if (typeof awaitHandlerSettlement !== 'function') {
17
22
  throw new TypeError('clean shutdown requires handler settlement');
18
23
  }
19
24
 
20
- const deliveryDrain = deliveryBarrier.fenceAndDrain();
21
- const processRetirement = pm.retireForCleanRestart({
25
+ const deliveryDrain = captureSettlement(() => deliveryBarrier.fenceAndDrain());
26
+ const processRetirement = captureSettlement(() => pm.retireForCleanRestart({
22
27
  getDeliveryEvidence: (sessionKey, sourceMsgId) => (
23
28
  deliveryBarrier.inspect(sessionKey, sourceMsgId)
24
29
  ),
25
- });
26
- const [, snapshots] = await Promise.all([
30
+ ...(qualificationExpectation !== undefined
31
+ ? { qualificationExpectation }
32
+ : {}),
33
+ }));
34
+ const [deliveryResult, retirementResult] = await Promise.allSettled([
27
35
  deliveryDrain,
28
36
  processRetirement,
29
37
  ]);
30
- await awaitHandlerSettlement({ timeoutMs: settlementTimeoutMs });
31
- return { snapshots: Array.isArray(snapshots) ? snapshots : [] };
38
+
39
+ let fallbackResult = null;
40
+ if (retirementResult.status === 'rejected') {
41
+ fallbackResult = await settleOne(() => {
42
+ if (typeof pm?.shutdown !== 'function') {
43
+ throw new TypeError('ProcessManager.shutdown fallback is required');
44
+ }
45
+ return pm.shutdown();
46
+ });
47
+ }
48
+
49
+ const ingressResult = await settleOne(() => (
50
+ awaitIngressSettlement({ timeoutMs: settlementTimeoutMs })
51
+ ));
52
+ const handlerResult = await settleOne(() => (
53
+ awaitHandlerSettlement({ timeoutMs: settlementTimeoutMs })
54
+ ));
55
+
56
+ throwFirstRejected([
57
+ retirementResult,
58
+ deliveryResult,
59
+ fallbackResult,
60
+ ingressResult,
61
+ handlerResult,
62
+ ]);
63
+ const snapshots = retirementResult.value;
64
+ const normalizedSnapshots = Array.isArray(snapshots) ? snapshots : [];
65
+ return {
66
+ snapshots: normalizedSnapshots,
67
+ qualification: Object.hasOwn(normalizedSnapshots, 'qualification')
68
+ ? normalizedSnapshots.qualification
69
+ : null,
70
+ };
71
+ }
72
+
73
+ async function settleCrashShutdown({
74
+ pm,
75
+ deliveryBarrier,
76
+ awaitIngressSettlement,
77
+ awaitHandlerSettlement,
78
+ settlementTimeoutMs = 30_000,
79
+ } = {}) {
80
+ if (typeof deliveryBarrier?.fenceAndDrain !== 'function') {
81
+ throw new TypeError('crash shutdown requires a delivery barrier');
82
+ }
83
+ if (typeof pm?.shutdown !== 'function') {
84
+ throw new TypeError('ProcessManager.shutdown is required');
85
+ }
86
+ if (typeof awaitIngressSettlement !== 'function') {
87
+ throw new TypeError('crash shutdown requires ingress settlement');
88
+ }
89
+ if (typeof awaitHandlerSettlement !== 'function') {
90
+ throw new TypeError('crash shutdown requires handler settlement');
91
+ }
92
+
93
+ const deliveryDrain = captureSettlement(() => deliveryBarrier.fenceAndDrain());
94
+ const processShutdown = captureSettlement(() => pm.shutdown());
95
+ const [deliveryResult, shutdownResult] = await Promise.allSettled([
96
+ deliveryDrain,
97
+ processShutdown,
98
+ ]);
99
+ const ingressResult = await settleOne(() => (
100
+ awaitIngressSettlement({ timeoutMs: settlementTimeoutMs })
101
+ ));
102
+ const handlerResult = await settleOne(() => (
103
+ awaitHandlerSettlement({ timeoutMs: settlementTimeoutMs })
104
+ ));
105
+
106
+ throwFirstRejected([
107
+ shutdownResult,
108
+ deliveryResult,
109
+ ingressResult,
110
+ handlerResult,
111
+ ]);
112
+ }
113
+
114
+ function captureSettlement(operation) {
115
+ try {
116
+ return Promise.resolve(operation());
117
+ } catch (error) {
118
+ return Promise.reject(error);
119
+ }
120
+ }
121
+
122
+ async function settleOne(operation) {
123
+ const [result] = await Promise.allSettled([captureSettlement(operation)]);
124
+ return result;
125
+ }
126
+
127
+ function throwFirstRejected(results) {
128
+ const rejected = results.find((result) => result?.status === 'rejected');
129
+ if (rejected) throw rejected.reason;
32
130
  }
33
131
 
34
132
  function buildResumeIntents({
@@ -116,5 +214,6 @@ function buildResumeIntents({
116
214
 
117
215
  module.exports = {
118
216
  prepareCleanRetirement,
217
+ settleCrashShutdown,
119
218
  buildResumeIntents,
120
219
  };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+
6
+ 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}$/;
7
+ const PACKAGE_VERSION_RE = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,127}$/;
8
+
9
+ function createDaemonIdentity({
10
+ mainPath,
11
+ packageVersion,
12
+ pid = process.pid,
13
+ randomUUID = crypto.randomUUID,
14
+ realpathSync = fs.realpathSync,
15
+ } = {}) {
16
+ if (typeof mainPath !== 'string' || mainPath.length === 0) {
17
+ throw new TypeError('daemon main path is required');
18
+ }
19
+ if (!PACKAGE_VERSION_RE.test(packageVersion || '')) {
20
+ throw new TypeError('daemon package version is invalid');
21
+ }
22
+ if (!Number.isSafeInteger(pid) || pid <= 1) {
23
+ throw new TypeError('daemon PID is invalid');
24
+ }
25
+
26
+ const daemonInstanceId = randomUUID();
27
+ if (!UUID_RE.test(daemonInstanceId)) {
28
+ throw new TypeError('daemon instance identity is invalid');
29
+ }
30
+ const mainRealpath = realpathSync(mainPath);
31
+
32
+ return Object.freeze({
33
+ pid,
34
+ daemon_instance_id: daemonInstanceId,
35
+ package_version: packageVersion,
36
+ main_realpath_sha256: crypto.createHash('sha256')
37
+ .update(mainRealpath)
38
+ .digest('hex'),
39
+ });
40
+ }
41
+
42
+ module.exports = { createDaemonIdentity };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ requireRestartRequestId,
5
+ } = require('../ipc/restart-request-id');
6
+ const {
7
+ normalizeDeployQualificationExpectation,
8
+ } = require('./clean-restart-qualification');
9
+ const {
10
+ normalizeDeployForegroundExpectation,
11
+ } = require('./foreground-canary-target');
12
+
13
+ function createDeployRestartHandler({
14
+ getIsShuttingDown,
15
+ getPid,
16
+ foregroundCanaryAuthorizer,
17
+ persistForegroundCanaryAuthorization,
18
+ shutdown,
19
+ logger = console,
20
+ } = {}) {
21
+ if (typeof getIsShuttingDown !== 'function') {
22
+ throw new TypeError('deploy restart shutdown-state reader is required');
23
+ }
24
+ if (typeof getPid !== 'function') {
25
+ throw new TypeError('deploy restart PID reader is required');
26
+ }
27
+ if (typeof persistForegroundCanaryAuthorization !== 'function') {
28
+ throw new TypeError('foreground canary authorization persistence is required');
29
+ }
30
+ if (typeof shutdown !== 'function') {
31
+ throw new TypeError('deploy restart shutdown function is required');
32
+ }
33
+
34
+ function rejected(oldPid, restartRequestId, rejectionCode) {
35
+ return {
36
+ accepted: false,
37
+ old_pid: oldPid,
38
+ restart_request_id: restartRequestId,
39
+ rejection_code: rejectionCode,
40
+ };
41
+ }
42
+
43
+ return function requestDeployRestart(request) {
44
+ const oldPid = getPid();
45
+ const isForegroundRequest = request != null
46
+ && typeof request === 'object'
47
+ && Object.hasOwn(request, 'foreground_expectation');
48
+ if (getIsShuttingDown() && !isForegroundRequest) {
49
+ const error = new Error('shutdown already in progress');
50
+ error.code = 'SHUTDOWN_IN_PROGRESS';
51
+ throw error;
52
+ }
53
+ let restartRequestId;
54
+ try {
55
+ restartRequestId = requireRestartRequestId(request?.id);
56
+ } catch {
57
+ return rejected(oldPid, null, 'invalid-request');
58
+ }
59
+ if (getIsShuttingDown()) {
60
+ return rejected(oldPid, restartRequestId, 'shutdown-in-progress');
61
+ }
62
+
63
+ const qualificationExpectation = normalizeDeployQualificationExpectation(request);
64
+ const foregroundExpectation = normalizeDeployForegroundExpectation(request);
65
+ if (qualificationExpectation === null || foregroundExpectation === null) {
66
+ return rejected(oldPid, restartRequestId, 'invalid-request');
67
+ }
68
+
69
+ if (foregroundExpectation !== undefined) {
70
+ if (typeof foregroundCanaryAuthorizer?.authorizeRestart !== 'function') {
71
+ return rejected(oldPid, restartRequestId, 'target-unavailable');
72
+ }
73
+ const authorization = foregroundCanaryAuthorizer.authorizeRestart({
74
+ requestId: restartRequestId,
75
+ expectation: foregroundExpectation,
76
+ });
77
+ if (!authorization?.accepted) {
78
+ return rejected(
79
+ oldPid,
80
+ restartRequestId,
81
+ authorization?.rejectionCode || 'target-unavailable',
82
+ );
83
+ }
84
+ try {
85
+ persistForegroundCanaryAuthorization(authorization.authorizationEvent);
86
+ } catch {
87
+ return rejected(
88
+ oldPid,
89
+ restartRequestId,
90
+ 'authorization-evidence-unavailable',
91
+ );
92
+ }
93
+ }
94
+
95
+ shutdown({
96
+ continuationAuthorized: true,
97
+ trigger: 'deploy-ipc',
98
+ restartRequestId,
99
+ qualificationExpectation,
100
+ }).catch((error) => {
101
+ logger.error?.(`[shutdown] deploy restart failed: ${error.message}`);
102
+ });
103
+ return {
104
+ accepted: true,
105
+ old_pid: oldPid,
106
+ restart_request_id: restartRequestId,
107
+ };
108
+ };
109
+ }
110
+
111
+ module.exports = { createDeployRestartHandler };