polygram 0.37.2 → 0.38.1
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 +4 -0
- package/lib/handlers/config-ui.js +53 -44
- package/lib/handlers/poll.js +138 -59
- package/lib/handlers/questions.js +26 -1
- package/lib/ipc/handlers.js +25 -0
- package/lib/ops/clean-restart-qualification.js +260 -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/systemd-invocation.js +10 -3
- package/lib/telegram/display-hint.js +1 -0
- package/package.json +2 -2
- package/polygram.js +206 -83
- package/scripts/ipc-smoke.js +38 -3
|
@@ -0,0 +1,260 @@
|
|
|
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
|
+
]);
|
|
134
|
+
const hasUnknownRequestField = requestKeys.some(
|
|
135
|
+
(key) => !allowedRequestKeys.has(key),
|
|
136
|
+
);
|
|
137
|
+
const hasExpectation = Object.hasOwn(request, 'qualification_expectation');
|
|
138
|
+
if (!hasExpectation && !hasUnknownRequestField) return undefined;
|
|
139
|
+
if (hasUnknownRequestField) return null;
|
|
140
|
+
|
|
141
|
+
const expectation = request.qualification_expectation;
|
|
142
|
+
if (!hasExactKeys(expectation, ['generation_digest', 'activity_epoch'])) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (
|
|
146
|
+
!SHA256_RE.test(expectation.generation_digest)
|
|
147
|
+
|| !Number.isSafeInteger(expectation.activity_epoch)
|
|
148
|
+
|| expectation.activity_epoch < 0
|
|
149
|
+
) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
expectedGenerationDigest: expectation.generation_digest,
|
|
154
|
+
expectedActivityEpoch: expectation.activity_epoch,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function projectQualificationResponse({
|
|
159
|
+
botName,
|
|
160
|
+
daemonIdentity,
|
|
161
|
+
observation,
|
|
162
|
+
now = Date.now,
|
|
163
|
+
} = {}) {
|
|
164
|
+
const normalized = normalizeQualificationObservation(observation, { now });
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
bot: botName,
|
|
167
|
+
daemon_instance_id: daemonIdentity?.daemon_instance_id ?? null,
|
|
168
|
+
package_version: daemonIdentity?.package_version ?? null,
|
|
169
|
+
observed_at_ms: normalized.observedAtMs,
|
|
170
|
+
generation_digest: normalized.generationDigest,
|
|
171
|
+
activity_epoch: normalized.activityEpoch,
|
|
172
|
+
process_state: normalized.processState,
|
|
173
|
+
active_turn_count: normalized.activeTurnCount,
|
|
174
|
+
pending_delivery_count: normalized.pendingDeliveryCount,
|
|
175
|
+
background_owner_count: normalized.backgroundOwnerCount,
|
|
176
|
+
background_terminal_count: normalized.backgroundTerminalCount,
|
|
177
|
+
background_terminal_registry_complete: normalized.backgroundTerminalRegistryComplete,
|
|
178
|
+
outcome_code: normalized.reason,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildQualificationEvent({
|
|
183
|
+
botName,
|
|
184
|
+
daemonIdentity,
|
|
185
|
+
restartRequestId,
|
|
186
|
+
expectation,
|
|
187
|
+
observation,
|
|
188
|
+
now = Date.now,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const normalized = normalizeQualificationObservation(observation, { now });
|
|
191
|
+
const expectationValid = (
|
|
192
|
+
isPlainObject(expectation)
|
|
193
|
+
&& hasExactKeys(expectation, [
|
|
194
|
+
'expectedGenerationDigest',
|
|
195
|
+
'expectedActivityEpoch',
|
|
196
|
+
])
|
|
197
|
+
&& SHA256_RE.test(expectation.expectedGenerationDigest)
|
|
198
|
+
&& Number.isSafeInteger(expectation.expectedActivityEpoch)
|
|
199
|
+
&& expectation.expectedActivityEpoch >= 0
|
|
200
|
+
);
|
|
201
|
+
const exactMatch = Boolean(
|
|
202
|
+
expectationValid
|
|
203
|
+
&& normalized.outcome === 'qualified'
|
|
204
|
+
&& normalized.reason === 'eligible'
|
|
205
|
+
&& normalized.generationDigest === expectation.expectedGenerationDigest
|
|
206
|
+
&& normalized.activityEpoch === expectation.expectedActivityEpoch
|
|
207
|
+
&& normalized.processState === 'Idle'
|
|
208
|
+
&& normalized.activeTurnCount === 0
|
|
209
|
+
&& normalized.pendingDeliveryCount === 0
|
|
210
|
+
&& normalized.backgroundOwnerCount === 0
|
|
211
|
+
&& normalized.backgroundTerminalCount === 0
|
|
212
|
+
&& normalized.backgroundTerminalRegistryComplete === true
|
|
213
|
+
);
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
bot: botName,
|
|
216
|
+
restart_request_sha256: createHash('sha256')
|
|
217
|
+
.update(restartRequestId)
|
|
218
|
+
.digest('hex'),
|
|
219
|
+
daemon_instance_id: daemonIdentity?.daemon_instance_id ?? null,
|
|
220
|
+
package_version: daemonIdentity?.package_version ?? null,
|
|
221
|
+
observed_at_ms: normalized.observedAtMs,
|
|
222
|
+
generation_digest: normalized.generationDigest,
|
|
223
|
+
expected_activity_epoch: expectationValid
|
|
224
|
+
? expectation.expectedActivityEpoch
|
|
225
|
+
: null,
|
|
226
|
+
observed_activity_epoch: normalized.activityEpoch,
|
|
227
|
+
process_state: normalized.processState,
|
|
228
|
+
active_turn_count: normalized.activeTurnCount,
|
|
229
|
+
pending_delivery_count: normalized.pendingDeliveryCount,
|
|
230
|
+
background_owner_count: normalized.backgroundOwnerCount,
|
|
231
|
+
background_terminal_count: normalized.backgroundTerminalCount,
|
|
232
|
+
background_terminal_registry_complete: normalized.backgroundTerminalRegistryComplete,
|
|
233
|
+
exact_match: exactMatch,
|
|
234
|
+
outcome_code: normalized.reason,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function assertClosedQualificationRequest(request) {
|
|
239
|
+
if (!isPlainObject(request)) {
|
|
240
|
+
throw new TypeError('clean restart qualification request is invalid');
|
|
241
|
+
}
|
|
242
|
+
const allowed = new Set(['op', 'id', 'secret']);
|
|
243
|
+
if (
|
|
244
|
+
request.op !== 'clean_restart_qualification'
|
|
245
|
+
|| Object.keys(request).some((key) => !allowed.has(key))
|
|
246
|
+
) {
|
|
247
|
+
const error = new Error('clean restart qualification request has unknown fields');
|
|
248
|
+
error.code = 'INVALID_CLEAN_RESTART_QUALIFICATION_REQUEST';
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
return request;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
assertClosedQualificationRequest,
|
|
256
|
+
buildQualificationEvent,
|
|
257
|
+
normalizeDeployQualificationExpectation,
|
|
258
|
+
normalizeQualificationObservation,
|
|
259
|
+
projectQualificationResponse,
|
|
260
|
+
};
|
package/lib/ops/clean-resume.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
30
|
+
...(qualificationExpectation !== undefined
|
|
31
|
+
? { qualificationExpectation }
|
|
32
|
+
: {}),
|
|
33
|
+
}));
|
|
34
|
+
const [deliveryResult, retirementResult] = await Promise.allSettled([
|
|
27
35
|
deliveryDrain,
|
|
28
36
|
processRetirement,
|
|
29
37
|
]);
|
|
30
|
-
|
|
31
|
-
|
|
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 };
|
|
@@ -9,11 +9,18 @@ function parseSystemdInvocationId(env = process.env) {
|
|
|
9
9
|
return value;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function lifecycleDetail(detail, invocationId) {
|
|
12
|
+
function lifecycleDetail(detail, invocationId, daemonIdentity = null) {
|
|
13
13
|
const value = detail && typeof detail === 'object' ? detail : {};
|
|
14
|
-
|
|
14
|
+
const withDaemonIdentity = daemonIdentity == null
|
|
15
15
|
? value
|
|
16
|
-
: {
|
|
16
|
+
: {
|
|
17
|
+
...value,
|
|
18
|
+
daemon_instance_id: daemonIdentity.daemon_instance_id,
|
|
19
|
+
pid: daemonIdentity.pid,
|
|
20
|
+
};
|
|
21
|
+
return invocationId == null
|
|
22
|
+
? withDaemonIdentity
|
|
23
|
+
: { ...withDaemonIdentity, invocation_id: invocationId };
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
module.exports = {
|
|
@@ -90,6 +90,7 @@ const richSection = (inlineMedia) => [
|
|
|
90
90
|
'- A numbered sequence (steps, ranked facts, top-N) → a real ordered list (`1.` at line start, continuation lines indented under their item), not bold pseudo-items like `**1. First thing.**` on their own paragraphs. A real list renders with native numbering and hanging indentation; the bold fake renders as flat paragraphs that merely start with a digit.',
|
|
91
91
|
'- A list of items each with a yes/no/pending state (approve this? confirm that?) → a real task list (`- [ ] item`), not `• item` with an emoji glued on.',
|
|
92
92
|
'- A comparison or key/value summary → a real markdown table.',
|
|
93
|
+
'- Rows and columns you are laying out yourself → a real markdown table, never hand-aligned columns inside a ``` fence. A fence is not a table: its columns are just whitespace you typed, so the client cannot re-lay-out them to fit and on a narrow screen the rows break mid-row and the alignment collapses into noise. Keep fences for code, logs, and command output you are quoting verbatim — including output that is already table-shaped, which you reproduce exactly rather than retype as a markdown table.',
|
|
93
94
|
'- A quoted aside, caveat, or callout you want visually set apart → a real blockquote (`> like this`), not a bold prefix.',
|
|
94
95
|
'- A visual break between distinct sections of a long reply → a real divider (`---` on its own line), the same way you already reach for it, but now it renders as a divider instead of literal dashes.',
|
|
95
96
|
'- Verbose supporting detail (a diff, a log, raw output) you want available but not front-and-center → `<details><summary>Label</summary>...</details>`.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc/client.js",
|
|
6
6
|
"bin": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "0.2.123",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"@shumkov/orchestra": "0.10.
|
|
66
|
+
"@shumkov/orchestra": "0.10.16",
|
|
67
67
|
"better-sqlite3": "^12.9.0",
|
|
68
68
|
"grammy": "^1.42.0",
|
|
69
69
|
"markdown-it": "^14.1.1",
|