@awak-app/simy-cli 0.7.20 → 0.7.21
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 +57 -12
- package/package.json +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +9 -1
- package/src/codex-work-pattern-sync.js +243 -43
- package/src/console/sessions.js +6 -6
- package/src/delivery-hooks/command.js +8 -6
- package/src/delivery-hooks/lifecycle-recovery.js +82 -0
- package/src/delivery-hooks/local-supervisor.js +25 -2
- package/src/delivery-hooks/supervisor-activation.js +25 -0
- package/src/delivery-hooks/supervisor-service.js +2 -2
- package/src/delivery-hooks/supervisor-watchdog.js +11 -6
- package/src/desktop-host.js +19 -5
- package/src/index.js +1 -1
- package/src/session-supervision/claude-sessions.js +7 -7
- package/src/session-supervision/codex-app-server.js +19 -9
- package/src/session-supervision/codex-peer-opinion.js +128 -0
- package/src/session-supervision/index.js +21 -12
- package/src/session-supervision/remote-control.js +27 -0
- package/src/session-supervision/remote-errors.js +22 -0
- package/src/session-supervision/remote-hosts.js +17 -13
- package/src/session-supervision/remote-session-list.js +56 -0
- package/src/session-supervision/session-list.js +83 -23
package/README.md
CHANGED
|
@@ -341,8 +341,10 @@ seven days. Usage counters alone leave work time, idle time and utilization
|
|
|
341
341
|
unavailable; a single observed activity event is a measured zero-duration sample.
|
|
342
342
|
Missing usage records remain distinct from recorded zero tokens.
|
|
343
343
|
|
|
344
|
-
The desktop Sessions page loads
|
|
345
|
-
|
|
344
|
+
The desktop Sessions page loads the full available Codex and Claude Code
|
|
345
|
+
conversation catalog without a 200-conversation display limit. Live process
|
|
346
|
+
probes remain limited to recent conversations and explicitly retained work;
|
|
347
|
+
older rows remain visible with unverified execution state. The macOS work list then prioritizes verified running
|
|
346
348
|
conversations, including families with running children, followed by work needing
|
|
347
349
|
a human response and recent activity. Its All, Running, Needs attention, Completed
|
|
348
350
|
and Archived views combine with search. Tracking and auto-continue settings never
|
|
@@ -362,13 +364,15 @@ Codex candidates come from CLI, VS Code and app-server
|
|
|
362
364
|
conversation sources; non-interactive exec, unknown sources and delegated agents
|
|
363
365
|
are excluded. A hook observation alone cannot restore a filtered conversation.
|
|
364
366
|
Explicitly tracked, monitored, auto-continue and unsettled supervision records
|
|
365
|
-
remain visible even
|
|
367
|
+
remain visible even during a provider discovery failure. When a retained Codex conversation is
|
|
366
368
|
omitted from the recent provider page, SIMY reads its exact metadata without
|
|
367
369
|
loading turns or taking writer ownership. The same interactive source filter and
|
|
368
370
|
a matching active rollout path are required before restoring provider capabilities
|
|
369
371
|
and probing its latest turn. Exact reads and turn probes share a bounded
|
|
370
372
|
observation deadline. Claude discovery
|
|
371
|
-
|
|
373
|
+
reads metadata with a bounded I/O pool and bounded transcript prefixes. Full
|
|
374
|
+
catalog reads include every project and transcript, while exact conversation
|
|
375
|
+
content and statistics remain separate reads. A conflicting saved
|
|
372
376
|
workspace leaves only that conversation's Action association unavailable; it
|
|
373
377
|
does not stop discovery or change the saved identity. The page lets users
|
|
374
378
|
choose monitoring or supported session supervision with a goal, acceptance
|
|
@@ -1304,9 +1308,9 @@ original machine. See [setup, access requirements and recovery](docs/remote-code
|
|
|
1304
1308
|
|
|
1305
1309
|
### Local conversation list on NVIDIA / Linux
|
|
1306
1310
|
|
|
1307
|
-
Run `simy sessions`
|
|
1308
|
-
|
|
1309
|
-
updater, analysis worker or a Delivery Loop.
|
|
1311
|
+
Run `simy sessions` to list local and explicitly registered SSH hosts. It reads
|
|
1312
|
+
both providers' actual conversation metadata without starting the agent,
|
|
1313
|
+
updater, analysis worker or a Delivery Loop. Remote reads use SSH with the existing user identity and verified host key.
|
|
1310
1314
|
|
|
1311
1315
|
```sh
|
|
1312
1316
|
simy sessions --query NVIDIA
|
|
@@ -1315,12 +1319,28 @@ simy sessions --json
|
|
|
1315
1319
|
```
|
|
1316
1320
|
|
|
1317
1321
|
In the interactive list, use Up/Down to select a conversation, `/` to search its
|
|
1318
|
-
title, folder or ID, Enter to finish searching, Esc to clear the filter, `r` to
|
|
1322
|
+
title, folder, provider or ID, Enter to finish searching, Esc to clear the filter, `r` to
|
|
1319
1323
|
refresh and `q` to quit. The selected conversation shows its exact ID and folder.
|
|
1320
|
-
Search covers the
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
+
Search covers the full available catalog, including older conversations. Use
|
|
1325
|
+
`--include-archived` to include archived conversations from both providers.
|
|
1326
|
+
Each row identifies its provider; identical IDs in Codex and Claude remain
|
|
1327
|
+
separate rows. A provider failure preserves the other provider's results with
|
|
1328
|
+
an explicit incomplete warning (`complete: false` and `warnings` in JSON).
|
|
1329
|
+
This view respects both saved provider connection settings and never enables
|
|
1330
|
+
automatic continuation. Disconnected providers are not read.
|
|
1331
|
+
|
|
1332
|
+
The terminal command reads this host and hosts explicitly registered in
|
|
1333
|
+
`~/.simy/remote-hosts.json`. Use `--local-only` to skip SSH, and
|
|
1334
|
+
`--provider codex` or `--provider claude` to select a provider. Remote SIMY must
|
|
1335
|
+
also include these options; older hosts produce an incomplete warning instead
|
|
1336
|
+
of silently disappearing. Each row retains its host, provider and conversation
|
|
1337
|
+
ID; search also matches the host label. SSH reads run `simy sessions --local-only`
|
|
1338
|
+
and do not start a desktop host, supervisor, or provider turn. Both the local
|
|
1339
|
+
and remote provider connection settings apply.
|
|
1340
|
+
|
|
1341
|
+
The desktop bridge also includes both providers. Neither interface imports
|
|
1342
|
+
Codex desktop connection settings automatically or claims to discover
|
|
1343
|
+
unregistered hosts, another OS user's history, or malformed/unsupported records.
|
|
1324
1344
|
|
|
1325
1345
|
The existing per-provider heartbeat now includes `device_io` with
|
|
1326
1346
|
`schema_version: local_device_io.v1`. It distinguishes Codex verified native-cwd
|
|
@@ -1350,3 +1370,28 @@ When supervision is already live, install the candidate, request
|
|
|
1350
1370
|
`runtime supervisor-handover --release ABSOLUTE_RELEASE`, and wait for activation.
|
|
1351
1371
|
The watcher preserves the live supervisor while service registration is updated.
|
|
1352
1372
|
No command here enrolls an unrelated conversation or renews an existing budget.
|
|
1373
|
+
|
|
1374
|
+
### Resume after sleep, logout or reboot
|
|
1375
|
+
|
|
1376
|
+
The OS watchdog retains activation in `supervisor-runtime/control.json`. Sleep
|
|
1377
|
+
re-observes the existing process before replacing it. After logout/login or a
|
|
1378
|
+
shutdown/reboot, the registered OS service loads the same runtime and saved task
|
|
1379
|
+
state. Linux uses a systemd user service (user lingering permits running after
|
|
1380
|
+
logout); macOS uses a login LaunchAgent; Windows uses the interactive user's
|
|
1381
|
+
scheduled task. macOS/Windows recovery therefore starts after that user logs in.
|
|
1382
|
+
No work runs while the computer is powered off or asleep.
|
|
1383
|
+
|
|
1384
|
+
To enable durable recovery for one existing opted-in conversation:
|
|
1385
|
+
|
|
1386
|
+
```sh
|
|
1387
|
+
~/.simy/bin/simy delivery runtime supervisor-enable --provider codex --session SESSION_ID
|
|
1388
|
+
~/.simy/bin/simy delivery runtime supervisor-watchdog-install
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
This selection binds its existing run and consent epoch, excluding other tasks.
|
|
1392
|
+
The recovered host starts by reading saved results and current artifacts. It
|
|
1393
|
+
retains the goal, budgets and unresolved effects; it does not replay an
|
|
1394
|
+
interrupted write or approve a pending external action. Explicit task
|
|
1395
|
+
stop/pause, provider permission prompts and unanswered user questions remain
|
|
1396
|
+
boundaries. `runtime supervisor-disable` is a durable explicit stop and is never
|
|
1397
|
+
reversed by waking or booting. A later explicit enable is required.
|
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.21";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
|
@@ -824,7 +824,7 @@ function hookDeliveryContext(record, automatic = false) {
|
|
|
824
824
|
`Preserve the current task and its permissions, pauses and stops; never restart or reset it automatically.`;
|
|
825
825
|
}
|
|
826
826
|
export async function startDelivery({ root = rootDirectory(), provider, session, cwd = process.cwd(), executionCwd, upgradeRunId, previousRunId, reconcileRunId, objective, nextStep, maxMinutes, maxContinuations, maxFailures, continuationPolicy, managed, admission,
|
|
827
|
-
liveHookAdmission, autoContinueEpoch, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
827
|
+
liveHookAdmission, autoContinueEpoch, followupAutoContinue = false, providerLinkEpoch, inspectProcess = processIdentity, now = Date.now() }) {
|
|
828
828
|
const link = await providerLinkState(root, provider);
|
|
829
829
|
if (providerLinkEpoch !== undefined && providerLinkEpoch !== link.epoch) throw Object.assign(new Error("SIMY provider connection changed before enrollment."), { code: "provider_link_changed" });
|
|
830
830
|
if (!link.connected) throw Object.assign(new Error("Reconnect this provider to SIMY before using it."), { code: "provider_link_disconnected" });
|
|
@@ -871,6 +871,7 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
871
871
|
if ((following || reconciling) && (provider !== 'codex' || !admitted || !managedState?.autoRecovery || managedState.capability !== 'codex_app_server' || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Follow-up requires exact-session managed supervision, a concrete goal and read-only reconciliation.');
|
|
872
872
|
if (upgrading && (!managedState?.autonomyPolicy || managedState.initialAction.kind !== 'read' || managedState.contextBootstrap)) throw new Error('Upgrade requires read-only reconciliation and frozen acceptance of the original objective.');
|
|
873
873
|
if (!managedState && continuationMode(continuationPolicy) === 'progress') throw new Error('Progress continuation requires frozen managed acceptance criteria.');
|
|
874
|
+
if (followupAutoContinue && !(following || reconciling)) throw new Error('Auto-continue follow-up requires the exact finished run.');
|
|
874
875
|
if (autoContinueEpoch && managedState) managedState.autoContinueEpoch = autoContinueEpoch;
|
|
875
876
|
if (managedState && previous.associationTaskId) {
|
|
876
877
|
managedState.taskId = previous.associationTaskId;
|
|
@@ -916,6 +917,13 @@ export async function startDelivery({ root = rootDirectory(), provider, session,
|
|
|
916
917
|
if (saved && digest(saved) !== digest(archive)) throw new Error('Finished run archive differs; reconcile its history before enrolling follow-up work.');
|
|
917
918
|
if (!saved) atomicSave(file, archive);
|
|
918
919
|
}
|
|
920
|
+
// The explicit follow-up dialog enrolls and saves ON in one transaction.
|
|
921
|
+
// A failed/stale request never leaves a half-enabled preference behind.
|
|
922
|
+
if (followupAutoContinue) {
|
|
923
|
+
const epoch = randomUUID();
|
|
924
|
+
managedState.autoContinueEpoch = epoch;
|
|
925
|
+
return { ...previous, autoContinue: true, autoContinueEpoch: epoch, run };
|
|
926
|
+
}
|
|
919
927
|
return { ...previous, run };
|
|
920
928
|
});
|
|
921
929
|
}
|
|
@@ -13,9 +13,11 @@ const MAX_MESSAGES = 24;
|
|
|
13
13
|
const MAX_MESSAGE_CHARS = 1_500;
|
|
14
14
|
const MAX_CANDIDATE_FILES = 200;
|
|
15
15
|
export const MAX_GATEWAY_BODY_BYTES = 512 * 1024;
|
|
16
|
+
const MAX_STATUS_RECEIPTS = 10;
|
|
17
|
+
const MAX_STATUS_BODY_BYTES = 12 * 1024;
|
|
16
18
|
const DEFAULT_LOOKBACK_DAYS = 90;
|
|
17
19
|
const POLL_INTERVAL_MS = 15 * 60 * 1_000;
|
|
18
|
-
const STATE_SCHEMA_VERSION = "work-pattern-history-sync.
|
|
20
|
+
const STATE_SCHEMA_VERSION = "work-pattern-history-sync.v7";
|
|
19
21
|
export const HISTORY_EXTRACTION_POLICY = "user_conversation.v2";
|
|
20
22
|
const PROVIDER_CODEX = "codex";
|
|
21
23
|
const PROVIDER_CLAUDE = "claude";
|
|
@@ -125,7 +127,20 @@ export async function syncCodexWorkPatterns({
|
|
|
125
127
|
deviceId,
|
|
126
128
|
});
|
|
127
129
|
const persistedState = await readSyncState(statePath);
|
|
128
|
-
const
|
|
130
|
+
const pendingReceipts = pendingReceiptMetadata(persistedState.pending_receipts);
|
|
131
|
+
if (pendingReceipts.length > 0) {
|
|
132
|
+
const pendingResult = await pollTimedOutReceipt({
|
|
133
|
+
endpoint,
|
|
134
|
+
token,
|
|
135
|
+
receipts: pendingReceipts,
|
|
136
|
+
fetchImpl,
|
|
137
|
+
signal,
|
|
138
|
+
authorizeProviders,
|
|
139
|
+
});
|
|
140
|
+
if (pendingResult.pending) return { status: "pending", uploaded: 0 };
|
|
141
|
+
}
|
|
142
|
+
const { pending_receipts: _pendingReceipts, ...resolvedPersistedState } = persistedState;
|
|
143
|
+
const previousState = statusPayload.reset_cursor === true ? {} : resolvedPersistedState;
|
|
129
144
|
const state = { ...previousState, providers: Object.fromEntries(Object.entries(previousState.providers || {})
|
|
130
145
|
.filter(([, cursor]) => cursor?.reset_pending !== true && (cursor?.lookback_days ?? previousState.lookback_days) === lookbackDays)) };
|
|
131
146
|
const lookbackCutoffMs = now - lookbackDays * 24 * 60 * 60 * 1_000;
|
|
@@ -175,10 +190,12 @@ export async function syncCodexWorkPatterns({
|
|
|
175
190
|
}))).flat();
|
|
176
191
|
const selectedFiles = selectSessionFiles(files);
|
|
177
192
|
const threads = [];
|
|
178
|
-
const acknowledgedRevisions = new Set(
|
|
193
|
+
const acknowledgedRevisions = new Set([
|
|
194
|
+
...providerSpecs.flatMap((spec) =>
|
|
179
195
|
Object.values(providerCursor(state, spec.provider).processed_files)
|
|
180
196
|
.filter((file) => file.lineage_ref && file.revision_ref)
|
|
181
|
-
.map((file) => `${file.lineage_ref}\0${file.revision_ref}`))
|
|
197
|
+
.map((file) => `${file.lineage_ref}\0${file.revision_ref}`)),
|
|
198
|
+
]);
|
|
182
199
|
const successfullyReadFiles = [];
|
|
183
200
|
const unreadableFiles = [];
|
|
184
201
|
for (const file of selectedFiles) {
|
|
@@ -212,6 +229,7 @@ export async function syncCodexWorkPatterns({
|
|
|
212
229
|
if (
|
|
213
230
|
chunk.messages.length > 0
|
|
214
231
|
&& Date.parse(chunk.observed_at) >= lookbackCutoffMs
|
|
232
|
+
&& !pendingReceipts.some(receipt => receiptKey(receipt) === threadReceiptKey(chunk))
|
|
215
233
|
) {
|
|
216
234
|
threads.push(chunk);
|
|
217
235
|
}
|
|
@@ -228,9 +246,38 @@ export async function syncCodexWorkPatterns({
|
|
|
228
246
|
return { status: "up_to_date", uploaded: 0 };
|
|
229
247
|
}
|
|
230
248
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
249
|
+
let uploadResult = { uploaded: 0 };
|
|
250
|
+
if (threads.length > 0) {
|
|
251
|
+
// Persist only opaque receipt metadata before an admission request that
|
|
252
|
+
// could reach Crew after this process loses its response. A restart polls
|
|
253
|
+
// these tuples and never reconstructs a plaintext retry from them.
|
|
254
|
+
let attempted = [...pendingReceipts];
|
|
255
|
+
const recordAttempt = async (batch, remove = false) => {
|
|
256
|
+
const keys = new Set(batch.map(threadReceiptKey));
|
|
257
|
+
attempted = attempted.filter(receipt => !keys.has(receiptKey(receipt)));
|
|
258
|
+
if (!remove) attempted.push(...batch.map(receiptMetadata));
|
|
259
|
+
if (attempted.length) {
|
|
260
|
+
await writePendingReceiptState({ statePath, lookbackDays, providers: previousState.providers || {}, receipts: attempted, now });
|
|
261
|
+
} else {
|
|
262
|
+
await clearPendingReceiptState({ statePath, lookbackDays, providers: previousState.providers || {}, now });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
uploadResult = await uploadThreadBatches({ endpoint, token, threads, fetchImpl, signal, authorizeProviders, recordAttempt });
|
|
266
|
+
if (uploadResult.pending_receipts?.length > 0) uploadResult.pending_receipts = attempted;
|
|
267
|
+
}
|
|
268
|
+
if (uploadResult.pending_receipts?.length > 0) {
|
|
269
|
+
await writePendingReceiptState({
|
|
270
|
+
statePath,
|
|
271
|
+
lookbackDays,
|
|
272
|
+
providers: previousState.providers || {},
|
|
273
|
+
receipts: uploadResult.pending_receipts,
|
|
274
|
+
now,
|
|
275
|
+
});
|
|
276
|
+
return { status: "pending", uploaded: 0 };
|
|
277
|
+
}
|
|
278
|
+
if (threads.length > 0 && uploadResult.uploaded !== threads.length) {
|
|
279
|
+
return { status: "pending", uploaded: uploadResult.uploaded };
|
|
280
|
+
}
|
|
234
281
|
if (shouldAcknowledgeEmptyReset) {
|
|
235
282
|
const { payload } = await postThreadBatch({ endpoint, token, threads: [], fetchImpl, signal, authorizeProviders });
|
|
236
283
|
validateThreadBatchAcknowledgement(payload, []);
|
|
@@ -279,7 +326,7 @@ export function workPatternSyncStatePath({ sessionRoot, apiOrigin, accountId, de
|
|
|
279
326
|
return join(sessionRoot, "work-pattern-history-sync", `${scopeHash}.json`);
|
|
280
327
|
}
|
|
281
328
|
|
|
282
|
-
async function uploadThreadBatches({ endpoint, token, threads, fetchImpl, signal, authorizeProviders }) {
|
|
329
|
+
async function uploadThreadBatches({ endpoint, token, threads, fetchImpl, signal, authorizeProviders, recordAttempt }) {
|
|
283
330
|
const batches = [];
|
|
284
331
|
let batch = [];
|
|
285
332
|
for (const thread of threads) {
|
|
@@ -299,6 +346,7 @@ async function uploadThreadBatches({ endpoint, token, threads, fetchImpl, signal
|
|
|
299
346
|
if (batch.length > 0) batches.push(batch);
|
|
300
347
|
|
|
301
348
|
let uploaded = 0;
|
|
349
|
+
const pendingReceipts = [];
|
|
302
350
|
for (const current of batches) {
|
|
303
351
|
const result = await postBatchWithBounded413Recovery({
|
|
304
352
|
endpoint,
|
|
@@ -307,26 +355,45 @@ async function uploadThreadBatches({ endpoint, token, threads, fetchImpl, signal
|
|
|
307
355
|
fetchImpl,
|
|
308
356
|
signal,
|
|
309
357
|
authorizeProviders,
|
|
358
|
+
recordAttempt,
|
|
310
359
|
});
|
|
311
360
|
uploaded += result.uploaded;
|
|
361
|
+
if (result.pending_receipts?.length > 0) {
|
|
362
|
+
pendingReceipts.push(...result.pending_receipts);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
312
365
|
}
|
|
313
|
-
return
|
|
366
|
+
return pendingReceipts.length > 0
|
|
367
|
+
? { uploaded, pending_receipts: pendingReceipts }
|
|
368
|
+
: { uploaded };
|
|
314
369
|
}
|
|
315
370
|
|
|
316
|
-
async function postBatchWithBounded413Recovery({ endpoint, token, threads, fetchImpl, signal, authorizeProviders }) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
371
|
+
async function postBatchWithBounded413Recovery({ endpoint, token, threads, fetchImpl, signal, authorizeProviders, recordAttempt }) {
|
|
372
|
+
await recordAttempt(threads);
|
|
373
|
+
let response, payload;
|
|
374
|
+
try {
|
|
375
|
+
({ response, payload } = await postThreadBatch({
|
|
376
|
+
endpoint,
|
|
377
|
+
token,
|
|
378
|
+
threads,
|
|
379
|
+
fetchImpl,
|
|
380
|
+
signal,
|
|
381
|
+
allow413: true,
|
|
382
|
+
authorizeProviders,
|
|
383
|
+
}));
|
|
384
|
+
} catch (error) {
|
|
385
|
+
if (error?.definitePreAdmission === true) await recordAttempt(threads, true);
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
326
388
|
if (response.status !== 413) {
|
|
327
|
-
|
|
389
|
+
const partition = validateReceiptPartition(payload, threads.map(receiptMetadata));
|
|
390
|
+
if (partition.pending > 0 || payload?._receiptPending === true) {
|
|
391
|
+
return { uploaded: 0, pending_receipts: threads.map(receiptMetadata) };
|
|
392
|
+
}
|
|
393
|
+
if (partition.notFound > 0) throwIncompleteAcknowledgement();
|
|
328
394
|
return { uploaded: threads.length };
|
|
329
395
|
}
|
|
396
|
+
await recordAttempt(threads, true);
|
|
330
397
|
if (threads.length > 1) {
|
|
331
398
|
const middle = Math.ceil(threads.length / 2);
|
|
332
399
|
const left = await postBatchWithBounded413Recovery({
|
|
@@ -336,7 +403,9 @@ async function postBatchWithBounded413Recovery({ endpoint, token, threads, fetch
|
|
|
336
403
|
fetchImpl,
|
|
337
404
|
signal,
|
|
338
405
|
authorizeProviders,
|
|
406
|
+
recordAttempt,
|
|
339
407
|
});
|
|
408
|
+
if (left.pending_receipts?.length) return left;
|
|
340
409
|
const right = await postBatchWithBounded413Recovery({
|
|
341
410
|
endpoint,
|
|
342
411
|
token,
|
|
@@ -344,13 +413,19 @@ async function postBatchWithBounded413Recovery({ endpoint, token, threads, fetch
|
|
|
344
413
|
fetchImpl,
|
|
345
414
|
signal,
|
|
346
415
|
authorizeProviders,
|
|
416
|
+
recordAttempt,
|
|
347
417
|
});
|
|
348
|
-
return {
|
|
418
|
+
return {
|
|
419
|
+
uploaded: left.uploaded + right.uploaded,
|
|
420
|
+
pending_receipts: [...(left.pending_receipts || []), ...(right.pending_receipts || [])],
|
|
421
|
+
};
|
|
349
422
|
}
|
|
350
|
-
|
|
423
|
+
const error = new Error(
|
|
351
424
|
"Work-pattern observation chunk was rejected as too large; "
|
|
352
425
|
+ "the complete revision was not acknowledged and will be retried unchanged.",
|
|
353
426
|
);
|
|
427
|
+
error.definitePreAdmission = true;
|
|
428
|
+
throw error;
|
|
354
429
|
}
|
|
355
430
|
|
|
356
431
|
async function postThreadBatch({
|
|
@@ -366,46 +441,171 @@ async function postThreadBatch({
|
|
|
366
441
|
if (Buffer.byteLength(body, "utf8") > MAX_GATEWAY_BODY_BYTES) {
|
|
367
442
|
throw new Error("Work-pattern observation payload exceeds the local CLI gateway limit.");
|
|
368
443
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
444
|
+
try {
|
|
445
|
+
await authorizeProviders();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
error.definitePreAdmission = true;
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
let response;
|
|
451
|
+
try {
|
|
452
|
+
response = await fetchImpl(endpoint, {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers: webApiHeaders({ token }, { "Content-Type": "application/json" }),
|
|
455
|
+
body,
|
|
456
|
+
signal: signalWithTimeout(signal, 120_000),
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (signal?.aborted || !isTimeout(error)) throw error;
|
|
460
|
+
const polled = await pollTimedOutReceipt({ endpoint, token, threads, fetchImpl, signal, authorizeProviders });
|
|
461
|
+
return polled.pending
|
|
462
|
+
? { response: polled.response, payload: { ...polled.payload, _receiptPending: true } }
|
|
463
|
+
: polled;
|
|
464
|
+
}
|
|
376
465
|
const payload = await response.json().catch(() => null);
|
|
377
466
|
if (!response.ok && !(allow413 && response.status === 413)) {
|
|
378
|
-
|
|
467
|
+
if (response.status >= 500 || response.status === 408 || response.status === 429) {
|
|
468
|
+
const polled = await pollTimedOutReceipt({ endpoint, token, threads, fetchImpl, signal, authorizeProviders });
|
|
469
|
+
return polled.pending
|
|
470
|
+
? { response: polled.response, payload: { ...polled.payload, _receiptPending: true } }
|
|
471
|
+
: polled;
|
|
472
|
+
}
|
|
473
|
+
const error = new Error(webApiErrorMessage(payload, response.status));
|
|
474
|
+
error.definitePreAdmission = response.status >= 400 && response.status < 500 && response.status !== 409;
|
|
475
|
+
throw error;
|
|
379
476
|
}
|
|
380
477
|
return { response, payload };
|
|
381
478
|
}
|
|
382
479
|
|
|
480
|
+
function isTimeout(error) {
|
|
481
|
+
return error?.name === "TimeoutError" || error?.name === "AbortError";
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function pollTimedOutReceipt({ endpoint, token, threads, receipts, fetchImpl, signal, authorizeProviders }) {
|
|
485
|
+
const expectedReceipts = receipts || threads.map(receiptMetadata);
|
|
486
|
+
const batches = receiptBatches(expectedReceipts);
|
|
487
|
+
await authorizeProviders();
|
|
488
|
+
const responses = [];
|
|
489
|
+
let hasPending = false;
|
|
490
|
+
for (const batch of batches) {
|
|
491
|
+
const response = await fetchImpl(new URL("status/", endpoint), {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: webApiHeaders({ token }, { "Content-Type": "application/json" }),
|
|
494
|
+
body: JSON.stringify({ receipts: batch }),
|
|
495
|
+
signal: signalWithTimeout(signal, 5_000),
|
|
496
|
+
});
|
|
497
|
+
const payload = await response.json().catch(() => null);
|
|
498
|
+
if (!response.ok) throw new Error(webApiErrorMessage(payload, response.status));
|
|
499
|
+
const partition = validateReceiptPartition(payload, batch);
|
|
500
|
+
if (partition.notFound > 0) throwIncompleteAcknowledgement();
|
|
501
|
+
hasPending ||= partition.pending > 0;
|
|
502
|
+
responses.push({ response, payload });
|
|
503
|
+
}
|
|
504
|
+
const last = responses.at(-1);
|
|
505
|
+
return hasPending
|
|
506
|
+
? { response: last.response, payload: last.payload, pending: true }
|
|
507
|
+
: { response: last.response, payload: last.payload };
|
|
508
|
+
}
|
|
509
|
+
|
|
383
510
|
function validateThreadBatchAcknowledgement(payload, threads) {
|
|
511
|
+
const partition = validateReceiptPartition(payload, threads.map(receiptMetadata));
|
|
512
|
+
if (partition.pending > 0 || partition.notFound > 0) throwIncompleteAcknowledgement();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function validateReceiptPartition(payload, expectedReceipts) {
|
|
516
|
+
const accepted = Array.isArray(payload?.accepted_receipts) ? payload.accepted_receipts : [];
|
|
517
|
+
const pending = Array.isArray(payload?.pending_receipts) ? payload.pending_receipts : [];
|
|
518
|
+
const notFound = Array.isArray(payload?.not_found_receipts) ? payload.not_found_receipts : [];
|
|
384
519
|
if (
|
|
385
|
-
payload?.raw_content_persisted !== false
|
|
386
|
-
!Number.isInteger(payload?.accepted)
|
|
387
|
-
payload.accepted !==
|
|
388
|
-
!
|
|
389
|
-
payload.
|
|
390
|
-
)
|
|
520
|
+
payload?.raw_content_persisted !== false
|
|
521
|
+
|| !Number.isInteger(payload?.accepted)
|
|
522
|
+
|| payload.accepted !== accepted.length
|
|
523
|
+
|| (payload?.pending !== undefined && (!Number.isInteger(payload.pending) || payload.pending !== pending.length))
|
|
524
|
+
|| (payload?.not_found !== undefined && (!Number.isInteger(payload.not_found) || payload.not_found !== notFound.length))
|
|
525
|
+
) throwIncompleteAcknowledgement();
|
|
526
|
+
const expected = expectedReceipts.map(receiptKey).sort();
|
|
527
|
+
const actual = [...accepted, ...pending, ...notFound].map(receiptKey).sort();
|
|
528
|
+
if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
|
|
391
529
|
throwIncompleteAcknowledgement();
|
|
392
530
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
for (let index = 0; index < expected.length; index += 1) {
|
|
396
|
-
if (actual[index] !== expected[index]) throwIncompleteAcknowledgement();
|
|
397
|
-
}
|
|
531
|
+
if (new Set(actual).size !== actual.length) throwIncompleteAcknowledgement();
|
|
532
|
+
return { accepted: accepted.length, pending: pending.length, notFound: notFound.length };
|
|
398
533
|
}
|
|
399
534
|
|
|
400
535
|
function threadReceiptKey(thread) {
|
|
401
|
-
return receiptKey(
|
|
536
|
+
return receiptKey(receiptMetadata(thread));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function receiptMetadata(thread) {
|
|
540
|
+
return {
|
|
402
541
|
source_ref: thread.source_ref,
|
|
403
542
|
revision_ref: thread.revision_ref,
|
|
404
543
|
lineage_ref: thread.lineage_ref,
|
|
405
544
|
chunk_index: thread.chunk_index,
|
|
406
545
|
chunk_count: thread.chunk_count,
|
|
407
546
|
message_offset: thread.message_offset,
|
|
408
|
-
message_count: Array.isArray(thread.messages)
|
|
547
|
+
message_count: Array.isArray(thread.messages)
|
|
548
|
+
? thread.messages.length
|
|
549
|
+
: (Number.isInteger(thread.message_count) ? thread.message_count : -1),
|
|
550
|
+
extraction_policy: thread.extraction_policy,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function pendingReceiptMetadata(receipts) {
|
|
555
|
+
if (receipts === undefined) return [];
|
|
556
|
+
if (!Array.isArray(receipts)) {
|
|
557
|
+
throw new Error("Pending work-pattern receipt state is invalid; history was not resent.");
|
|
558
|
+
}
|
|
559
|
+
return receipts.map((receipt) => {
|
|
560
|
+
if (
|
|
561
|
+
!/^(?:codex|claude):[0-9a-f]{64}$/.test(receipt?.source_ref || "")
|
|
562
|
+
|| !/^(?:codex|claude):[0-9a-f]{64}$/.test(receipt?.lineage_ref || "")
|
|
563
|
+
|| !/^[0-9a-f]{64}$/.test(receipt?.revision_ref || "")
|
|
564
|
+
|| receipt?.extraction_policy !== HISTORY_EXTRACTION_POLICY
|
|
565
|
+
|| !Number.isInteger(receipt?.chunk_index)
|
|
566
|
+
|| !Number.isInteger(receipt?.chunk_count)
|
|
567
|
+
|| !Number.isInteger(receipt?.message_offset)
|
|
568
|
+
|| !Number.isInteger(receipt?.message_count)
|
|
569
|
+
) {
|
|
570
|
+
throw new Error("Pending work-pattern receipt state is invalid; history was not resent.");
|
|
571
|
+
}
|
|
572
|
+
return receipt;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function receiptBatches(receipts) {
|
|
577
|
+
const batches = [];
|
|
578
|
+
let batch = [];
|
|
579
|
+
for (const receipt of receipts) {
|
|
580
|
+
const candidate = [...batch, receipt];
|
|
581
|
+
if (candidate.length > MAX_STATUS_RECEIPTS || Buffer.byteLength(JSON.stringify({ receipts: candidate }), "utf8") > MAX_STATUS_BODY_BYTES) {
|
|
582
|
+
if (batch.length === 0) throw new Error("Work-pattern receipt metadata exceeds the local gateway limit.");
|
|
583
|
+
batches.push(batch);
|
|
584
|
+
batch = [receipt];
|
|
585
|
+
} else {
|
|
586
|
+
batch = candidate;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (batch.length > 0) batches.push(batch);
|
|
590
|
+
return batches;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function writePendingReceiptState({ statePath, lookbackDays, providers, receipts, now }) {
|
|
594
|
+
await writeSyncState(statePath, {
|
|
595
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
596
|
+
lookback_days: lookbackDays,
|
|
597
|
+
providers,
|
|
598
|
+
pending_receipts: receipts,
|
|
599
|
+
last_synced_at: new Date(now).toISOString(),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function clearPendingReceiptState({ statePath, lookbackDays, providers, now }) {
|
|
604
|
+
await writeSyncState(statePath, {
|
|
605
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
606
|
+
lookback_days: lookbackDays,
|
|
607
|
+
providers,
|
|
608
|
+
last_synced_at: new Date(now).toISOString(),
|
|
409
609
|
});
|
|
410
610
|
}
|
|
411
611
|
|
package/src/console/sessions.js
CHANGED
|
@@ -7,7 +7,7 @@ export function SessionListConsole({ catalog: initial, query: initialQuery = '',
|
|
|
7
7
|
const { exit } = useApp(), { stdout } = useStdout();
|
|
8
8
|
const [catalog, setCatalog] = useState(initial), [query, setQuery] = useState(initialQuery);
|
|
9
9
|
const [searching, setSearching] = useState(false), [index, setIndex] = useState(0);
|
|
10
|
-
const [busy, setBusy] = useState(false), [notice, setNotice] = useState('');
|
|
10
|
+
const [busy, setBusy] = useState(false), [notice, setNotice] = useState((initial.warnings || []).join('; '));
|
|
11
11
|
const [height, setHeight] = useState(stdout.rows || 24);
|
|
12
12
|
useEffect(() => { const resize = () => setHeight(stdout.rows || 24); stdout.on('resize', resize); return () => stdout.off('resize', resize); }, [stdout]);
|
|
13
13
|
const rows = filterSessionList(catalog.sessions, query), selected = Math.max(0, Math.min(index, Math.max(0, rows.length - 1)));
|
|
@@ -26,7 +26,7 @@ export function SessionListConsole({ catalog: initial, query: initialQuery = '',
|
|
|
26
26
|
if (key.downArrow) setIndex(Math.max(0, Math.min(rows.length - 1, selected + 1)));
|
|
27
27
|
if (text === 'r' && !busy) {
|
|
28
28
|
setBusy(true); setNotice('');
|
|
29
|
-
void refresh().then(next => { setCatalog(next); setIndex(0); }, error => {
|
|
29
|
+
void refresh().then(next => { setCatalog(next); setNotice((next.warnings || []).join('; ')); setIndex(0); }, error => {
|
|
30
30
|
if (['provider_link_disconnected', 'provider_link_changed'].includes(error.code)) {
|
|
31
31
|
setCatalog(current => ({ ...current, sessions: [] })); setIndex(0);
|
|
32
32
|
}
|
|
@@ -36,15 +36,15 @@ export function SessionListConsole({ catalog: initial, query: initialQuery = '',
|
|
|
36
36
|
});
|
|
37
37
|
const current = rows[selected];
|
|
38
38
|
return jsxs(Box, { flexDirection: 'column', children: [
|
|
39
|
-
jsx(Text, { bold: true, color: 'cyan', children: `SIMY · Codex会話一覧 · ${terminalText(catalog.host)}` }),
|
|
40
|
-
jsx(Text, { children: `表示 ${rows.length} / ${catalog.sessions.length} 件 ·
|
|
39
|
+
jsx(Text, { bold: true, color: 'cyan', children: `SIMY · Codex / Claude Code 会話一覧 · ${terminalText(catalog.host)}` }),
|
|
40
|
+
jsx(Text, { children: `表示 ${rows.length} / ${catalog.sessions.length} 件 · 接続先の実会話` }),
|
|
41
41
|
jsx(Text, { color: searching ? 'yellow' : undefined, children: `検索: ${query}${searching ? '▏' : ''}` }),
|
|
42
42
|
...rows.slice(offset, offset + pageSize).map((row, i) => jsx(Text, {
|
|
43
43
|
color: offset + i === selected ? 'green' : undefined, wrap: 'truncate-end',
|
|
44
44
|
children: `${offset + i === selected ? '›' : ' '} ${terminalText(row.title)}`,
|
|
45
|
-
}, row.session)),
|
|
45
|
+
}, `${row.hostId || 'local'}:${row.provider}:${row.session}`)),
|
|
46
46
|
...(!rows.length ? [jsx(Text, { children: '一致する会話はありません。' }, 'empty')] : []),
|
|
47
|
-
jsx(Text, { dimColor: true, children: current ? `${terminalText(current.workState?.label)} · ${current.session}` : '' }),
|
|
47
|
+
jsx(Text, { dimColor: true, children: current ? `${terminalText(current.hostLabel || catalog.host)} · ${current.provider === 'claude' ? 'Claude Code' : 'Codex'} · ${terminalText(current.workState?.label)} · ${current.session}` : '' }),
|
|
48
48
|
jsx(Text, { dimColor: true, wrap: 'truncate-end', children: terminalText(current?.cwd) }),
|
|
49
49
|
jsx(Text, { color: notice ? 'red' : undefined, children: notice || (busy ? '更新中…' : '↑↓ 選択 / 検索 Esc 解除 r 更新 q 終了') }),
|
|
50
50
|
] });
|
|
@@ -144,19 +144,20 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
144
144
|
output(JSON.stringify(result, null, 2)); return result.status === 'not_requested' ? 2 : 0;
|
|
145
145
|
}
|
|
146
146
|
if (['supervisor-enable', 'supervisor-disable', 'supervisor-watch', 'supervisor-status', 'supervisor-watchdog-install'].includes(args[0])) {
|
|
147
|
-
const options = parseDeliveryOptions(args.slice(1), new Set(['root']));
|
|
147
|
+
const options = parseDeliveryOptions(args.slice(1), new Set(['root', ...(args[0] === 'supervisor-enable' ? ['provider', 'session'] : [])]));
|
|
148
148
|
const supervisorRoot = options.root || dirname(root);
|
|
149
149
|
if (!isAbsolute(supervisorRoot)) throw new Error('Supervisor root must be absolute.');
|
|
150
150
|
if (args[0] === 'supervisor-watch') {
|
|
151
151
|
const controller = new AbortController(), interrupt = () => controller.abort('user_interrupt'), terminate = () => controller.abort('service_termination');
|
|
152
|
-
process.once('SIGINT', interrupt); process.once('SIGTERM', terminate);
|
|
152
|
+
process.once('SIGINT', interrupt); process.once('SIGTERM', terminate); process.once('SIGHUP', terminate);
|
|
153
153
|
try { await watchSupervisor({ root: supervisorRoot, signal: controller.signal }); }
|
|
154
|
-
finally { process.removeListener('SIGINT', interrupt); process.removeListener('SIGTERM', terminate); }
|
|
154
|
+
finally { process.removeListener('SIGINT', interrupt); process.removeListener('SIGTERM', terminate); process.removeListener('SIGHUP', terminate); }
|
|
155
155
|
return 0;
|
|
156
156
|
}
|
|
157
157
|
const result = args[0] === 'supervisor-status' ? await supervisorWatchdogStatus({ root: supervisorRoot })
|
|
158
158
|
: args[0] === 'supervisor-watchdog-install' ? await installSupervisorService({ root: supervisorRoot })
|
|
159
|
-
: await setSupervisorWatchdog({ root: supervisorRoot, enabled: args[0] === 'supervisor-enable', webOrigin
|
|
159
|
+
: await setSupervisorWatchdog({ root: supervisorRoot, enabled: args[0] === 'supervisor-enable', webOrigin,
|
|
160
|
+
...((options.provider || options.session) ? { target: { provider: options.provider, session: options.session } } : {}) });
|
|
160
161
|
output(JSON.stringify(result, null, 2)); return 0;
|
|
161
162
|
}
|
|
162
163
|
if (args[0] === 'policy-adopt') {
|
|
@@ -187,11 +188,12 @@ export async function runDeliveryCommand(argv, { root = rootDirectory(), cwd = p
|
|
|
187
188
|
return 0;
|
|
188
189
|
}
|
|
189
190
|
if (action === 'supervise') {
|
|
190
|
-
|
|
191
|
+
const once = args.includes('--once');
|
|
192
|
+
const options = parseDeliveryOptions(args.filter(arg => arg !== '--once'), new Set(['activation-epoch']));
|
|
191
193
|
const controller = new AbortController();
|
|
192
194
|
const stop = () => controller.abort();
|
|
193
195
|
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
194
|
-
try { await superviseLocalDelivery({ root, webOrigin, once:
|
|
196
|
+
try { await superviseLocalDelivery({ root, webOrigin, once, activationEpoch: options['activation-epoch'], signal: controller.signal }); }
|
|
195
197
|
finally { process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop); }
|
|
196
198
|
return 0;
|
|
197
199
|
}
|