@awak-app/simy-cli 0.7.32 → 0.7.34
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 +22 -3
- package/package.json +3 -2
- package/src/auto-update.js +10 -1
- package/src/cli-contract.js +1 -1
- package/src/cli-observability.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +44 -12
- package/src/codex-plugins/simy/hooks/purpose-workspace.mjs +13 -4
- package/src/delivery-hooks/desktop.js +1 -1
- package/src/desktop-access.js +1 -1
- package/src/desktop-frame-writer.js +29 -0
- package/src/desktop-host.js +252 -77
- package/src/orchestrator/loop.js +2 -0
- package/src/session-supervision/analysis-manager.js +83 -18
- package/src/session-supervision/claude-desktop-lineage.js +1 -1
- package/src/session-supervision/claude-sessions.js +16 -4
- package/src/session-supervision/codex-app-server.js +45 -30
- package/src/session-supervision/continuation-readiness.js +58 -0
- package/src/session-supervision/decision-presentation.js +11 -3
- package/src/session-supervision/index.js +126 -28
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +79 -0
- package/src/session-supervision/remote-entry.js +7 -4
- package/src/session-supervision/remote-host-recovery.js +21 -0
- package/src/session-supervision/remote-hosts.js +28 -8
- package/src/session-supervision/remote-owner-entry.js +20 -0
- package/src/session-supervision/remote-service.js +36 -0
- package/src/session-supervision/statistics.js +19 -3
- package/src/session-supervision/work-state.js +7 -1
- package/src/sqm/local-tests.js +2 -1
- package/src/update-connection.js +16 -0
package/README.md
CHANGED
|
@@ -969,9 +969,20 @@ diagrams. This is a design proposal, not a shipped completion guarantee.
|
|
|
969
969
|
Publishing is handled by GitHub Actions in `.github/workflows/publish.yml`.
|
|
970
970
|
|
|
971
971
|
- Every push tag matching `v*.*.*` runs checks and publishes to npm.
|
|
972
|
-
- Manual workflow runs perform the same checks
|
|
973
|
-
|
|
974
|
-
|
|
972
|
+
- Manual workflow runs perform the same checks and verify the npm OIDC exchange.
|
|
973
|
+
Leave `publish=false` for a dry-run; set `publish=true` to publish.
|
|
974
|
+
- npm trusted publishing must allow direct `npm publish` for GitHub organization
|
|
975
|
+
`SpecialistDoctors-Inc`, repository `simy-cli`, workflow filename `publish.yml`,
|
|
976
|
+
with no environment restriction (this job does not use a GitHub environment).
|
|
977
|
+
Publishing uses GitHub-hosted runners, Node 22 and npm 11.21.0.
|
|
978
|
+
- The publisher verifies GitHub OIDC issuance and npm token exchange separately,
|
|
979
|
+
without logging or persisting credentials. It never falls back to `NPM_TOKEN`;
|
|
980
|
+
local npm login is not required for CI publishing.
|
|
981
|
+
- A registry outage can also cause an OIDC exchange failure. Check npm service
|
|
982
|
+
status and the failing preflight stage before changing account settings.
|
|
983
|
+
Rerunning a tag uses that tag's workflow; it does not pick up later fixes.
|
|
984
|
+
Retry only the npm job for an already released native version, and check the
|
|
985
|
+
current npm `latest` first so an older release cannot replace a newer one.
|
|
975
986
|
- The package check inspects the final npm tarball contents and fails if any
|
|
976
987
|
source map file would be published.
|
|
977
988
|
|
|
@@ -1518,3 +1529,11 @@ that operation continues to block writes. Observing the result settles local
|
|
|
1518
1529
|
occupancy without accepting the task. Long-running tasks transmit current
|
|
1519
1530
|
recovery context and recent work; the full history remains in durable state
|
|
1520
1531
|
and can be inspected through `delivery status`.
|
|
1532
|
+
|
|
1533
|
+
### Reusable Auto Continue evaluation inputs
|
|
1534
|
+
|
|
1535
|
+
For the 20 versioned conversation regression cases, immutable stopping-protocol
|
|
1536
|
+
plans, and pinned SWE-bench Pro public-task preparation, see
|
|
1537
|
+
[the evaluation guide](scripts/auto-continue-eval/README.md). Run
|
|
1538
|
+
`npm run eval:auto-continue -- help` from a source checkout. This tooling prepares
|
|
1539
|
+
inputs; it does not launch models or establish continuation effectiveness.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.34",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"test:e2e:claude-plugin": "node ./scripts/claude-plugin-runtime-e2e.js",
|
|
55
55
|
"test:e2e:codex-plugin": "node ./scripts/codex-plugin-runtime-e2e.js",
|
|
56
56
|
"test:e2e:flexible-delivery": "node --test --test-name-pattern=\"flexible prototype\" test/purpose-search.test.js",
|
|
57
|
-
"test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js"
|
|
57
|
+
"test:e2e:sqm-faults": "node ./scripts/sqm-complete-loop-e2e.js",
|
|
58
|
+
"eval:auto-continue": "node ./scripts/auto-continue-eval.js"
|
|
58
59
|
},
|
|
59
60
|
"engines": {
|
|
60
61
|
"node": ">=20"
|
package/src/auto-update.js
CHANGED
|
@@ -16,6 +16,9 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
|
|
18
18
|
import { daemonRestartCommand } from "./web-origin.js";
|
|
19
|
+
import { updateConnectionEnvironment } from './update-connection.js';
|
|
20
|
+
import { connectionScope, assertConnectionScope } from './connection-diagnostics.js';
|
|
21
|
+
import { readSession } from './session-store.js';
|
|
19
22
|
import { openExternalUrl } from "./browser.js";
|
|
20
23
|
import {
|
|
21
24
|
installMacosInstallerRelease,
|
|
@@ -1006,6 +1009,7 @@ export async function restartDaemonProcess({
|
|
|
1006
1009
|
env = process.env,
|
|
1007
1010
|
} = {}) {
|
|
1008
1011
|
const handoffDirectory = path.join(stateRoot, "updates", "handoff");
|
|
1012
|
+
const expectedScope = expectedWebOrigin ? connectionScope(await readSession(expectedWebOrigin, stateRoot), expectedWebOrigin) : null;
|
|
1009
1013
|
await mkdir(handoffDirectory, { recursive: true, mode: 0o700 });
|
|
1010
1014
|
const nonce = randomBytes(24).toString("hex");
|
|
1011
1015
|
const handoffPath = path.join(handoffDirectory, `${process.pid}-${nonce}.json`);
|
|
@@ -1013,7 +1017,7 @@ export async function restartDaemonProcess({
|
|
|
1013
1017
|
detached: true,
|
|
1014
1018
|
stdio: "ignore",
|
|
1015
1019
|
env: {
|
|
1016
|
-
...env,
|
|
1020
|
+
...(expectedWebOrigin ? updateConnectionEnvironment({ env, root: stateRoot, webOrigin: expectedWebOrigin }) : { ...env, SIMY_HOME: path.resolve(stateRoot) }),
|
|
1017
1021
|
SIMY_DAEMON_CHILD: "1",
|
|
1018
1022
|
SIMY_UPDATE_HANDOFF_FILE: handoffPath,
|
|
1019
1023
|
SIMY_UPDATE_HANDOFF_NONCE: nonce,
|
|
@@ -1040,6 +1044,10 @@ export async function restartDaemonProcess({
|
|
|
1040
1044
|
if (expectedInstallMode && handoff.install_mode !== expectedInstallMode) {
|
|
1041
1045
|
throw new Error("Restarted SIMY reported a different installation channel.");
|
|
1042
1046
|
}
|
|
1047
|
+
if (expectedScope?.account_id || expectedScope?.organization_id || expectedScope?.device_id) {
|
|
1048
|
+
assertConnectionScope(expectedScope, handoff.scope);
|
|
1049
|
+
assertConnectionScope(expectedScope, connectionScope(await readSession(expectedWebOrigin, stateRoot), expectedWebOrigin));
|
|
1050
|
+
}
|
|
1043
1051
|
return handoff;
|
|
1044
1052
|
} catch (error) {
|
|
1045
1053
|
await terminateCandidate(child);
|
|
@@ -1073,6 +1081,7 @@ export async function writeUpdateHandoffFromEnvironment({
|
|
|
1073
1081
|
port,
|
|
1074
1082
|
pid: process.pid,
|
|
1075
1083
|
web_origin: webOrigin,
|
|
1084
|
+
scope: webOrigin ? connectionScope(await readSession(webOrigin, stateRoot), webOrigin) : null,
|
|
1076
1085
|
install_mode: installMode,
|
|
1077
1086
|
ready_at: new Date().toISOString(),
|
|
1078
1087
|
})}\n`,
|
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.34";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
package/src/cli-observability.js
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from 'node:child_process';
|
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { webApiHeaders, webApiUrl } from './web-api.js';
|
|
9
9
|
// Keep this leaf module package-safe: hooks load it before the full CLI contract.
|
|
10
|
-
const CLI_VERSION = '0.7.
|
|
10
|
+
const CLI_VERSION = '0.7.34';
|
|
11
11
|
|
|
12
12
|
const STORE = 'cli-observability-state.json';
|
|
13
13
|
const LEGACY_CONFIG = 'cli-observability.json';
|
|
@@ -340,7 +340,8 @@ export async function readRecord(root = rootDirectory(), provider, session) {
|
|
|
340
340
|
} catch { return null; } // Match listRecords' treatment of unreadable records.
|
|
341
341
|
}
|
|
342
342
|
const RECORD_NAME = /^[a-f0-9]{64}\.json$/;
|
|
343
|
-
const RECORD_CACHE_LIMIT =
|
|
343
|
+
const RECORD_CACHE_LIMIT = 10_000;
|
|
344
|
+
const RECORD_CACHE_BYTES = 32 * 1024 * 1024;
|
|
344
345
|
const READ_CONCURRENCY = 8;
|
|
345
346
|
async function recordNames(root) {
|
|
346
347
|
try { return (await readdir(root)).filter(name => RECORD_NAME.test(name)).sort(); }
|
|
@@ -367,13 +368,16 @@ export async function listRecords(root = rootDirectory()) {
|
|
|
367
368
|
// A shared candidate/display index, never mutation authority. Every list checks
|
|
368
369
|
// metadata so external hooks, removals and direct writes invalidate cached JSON.
|
|
369
370
|
// Fresh execution reads and updateRecord locks remain independent of this index.
|
|
370
|
-
export function createRecordReader(root = rootDirectory()) {
|
|
371
|
+
export function createRecordReader(root = rootDirectory(), { cacheEntries = RECORD_CACHE_LIMIT, cacheBytes = RECORD_CACHE_BYTES } = {}) {
|
|
372
|
+
if (!Number.isSafeInteger(cacheEntries) || cacheEntries < 1 || cacheEntries > RECORD_CACHE_LIMIT ||
|
|
373
|
+
!Number.isSafeInteger(cacheBytes) || cacheBytes < 1 || cacheBytes > RECORD_CACHE_BYTES) throw new Error('Invalid record cache limits.');
|
|
371
374
|
const cache = new Map();
|
|
372
|
-
let inFlight = null, closed = false;
|
|
375
|
+
let inFlight = null, closed = false, retainedBytes = 0;
|
|
376
|
+
const discard = name => { retainedBytes -= cache.get(name)?.bytes || 0; cache.delete(name); };
|
|
373
377
|
const fingerprint = info => [info.dev, info.ino, info.size, info.mtimeNs, info.ctimeNs].join(':');
|
|
374
378
|
const scan = async () => {
|
|
375
|
-
const names = await recordNames(root), live = new Set(names.slice(0,
|
|
376
|
-
for (const name of cache.keys()) if (!live.has(name))
|
|
379
|
+
const names = await recordNames(root), live = new Set(names.slice(0, cacheEntries));
|
|
380
|
+
for (const name of cache.keys()) if (!live.has(name)) discard(name);
|
|
377
381
|
return mapRecordNames(names, async name => {
|
|
378
382
|
const path = join(root, name);
|
|
379
383
|
try {
|
|
@@ -384,10 +388,14 @@ export function createRecordReader(root = rootDirectory()) {
|
|
|
384
388
|
// Do not retain a value when a write races the read; the next list must
|
|
385
389
|
// inspect it again. This also prevents malformed transient JSON sticking.
|
|
386
390
|
const after = await stat(path, { bigint: true });
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
discard(name);
|
|
392
|
+
const bytes = Number(after.size);
|
|
393
|
+
if (live.has(name) && bytes <= cacheBytes && fingerprint(after) === key) {
|
|
394
|
+
while (retainedBytes + bytes > cacheBytes && cache.size) discard(cache.keys().next().value);
|
|
395
|
+
cache.set(name, { key, record, bytes }); retainedBytes += bytes;
|
|
396
|
+
}
|
|
389
397
|
return record;
|
|
390
|
-
} catch {
|
|
398
|
+
} catch { discard(name); return null; }
|
|
391
399
|
});
|
|
392
400
|
};
|
|
393
401
|
return {
|
|
@@ -398,7 +406,7 @@ export function createRecordReader(root = rootDirectory()) {
|
|
|
398
406
|
// cached records or a simultaneous caller's view.
|
|
399
407
|
return currentProviderConsent(root, structuredClone(await inFlight));
|
|
400
408
|
},
|
|
401
|
-
async close() { closed = true; await inFlight?.catch(() => {}); cache.clear(); },
|
|
409
|
+
async close() { closed = true; await inFlight?.catch(() => {}); cache.clear(); retainedBytes = 0; },
|
|
402
410
|
};
|
|
403
411
|
}
|
|
404
412
|
function positive(value, fallback, max) {
|
|
@@ -1115,7 +1123,7 @@ export async function enablePurposeDelivery({ root = rootDirectory(), provider,
|
|
|
1115
1123
|
if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted; automatic continuation cannot reset it.');
|
|
1116
1124
|
if (!run.managed) {
|
|
1117
1125
|
const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] }, 'Read the original conversation, reconstruct its purpose and completion conditions, then choose necessary work.');
|
|
1118
|
-
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: '
|
|
1126
|
+
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced', approvalScopes: ['task'], approvalPolicy: 'wait',
|
|
1119
1127
|
initialPlan: plan, initialAction: plan.alternatives[0].action, autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: admission.model, purpose },
|
|
1120
1128
|
session, provider, record.process, admission, now);
|
|
1121
1129
|
run.managed.purpose.previousCheckpoint = run.checkpoint;
|
|
@@ -2089,7 +2097,9 @@ export function deliveryStopBlocksResume(run, now = Date.now()) {
|
|
|
2089
2097
|
}
|
|
2090
2098
|
// Stopped remains terminal for hooks, polling and automatic recovery. Only the
|
|
2091
2099
|
// explicit control action may reopen an enrolled app-server task for diagnosis.
|
|
2092
|
-
export function canExplicitlyResumeStopped(run) {
|
|
2100
|
+
export function canExplicitlyResumeStopped(run, { standard = false } = {}) {
|
|
2101
|
+
if (standard && run?.state === 'stopped' && !run.managed)
|
|
2102
|
+
return canUpgradeDelivery({ ...run, state: 'active' }) && (run.failures || 0) < run.maxFailures;
|
|
2093
2103
|
return run?.state === 'stopped' && run.managed?.resumeChannel === 'codex_app_server' &&
|
|
2094
2104
|
Boolean(run.managed.autonomyPolicy && run.managed.approvalScopes?.length) &&
|
|
2095
2105
|
!run.managed.resumeLease && !run.managed.liveWorkspaceLease;
|
|
@@ -2977,8 +2987,30 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2977
2987
|
run.providerRequest = null;
|
|
2978
2988
|
const marker = readState(stopMarkerPath(root, provider, session));
|
|
2979
2989
|
if (marker?.runId === run.id) run.handledStopMarkerId = marker.id;
|
|
2980
|
-
const wasStopped = canExplicitlyResumeStopped(run);
|
|
2990
|
+
const wasStopped = canExplicitlyResumeStopped(run, { standard: provider === 'codex' });
|
|
2981
2991
|
const wasVerifying = canResumePendingVerification(run);
|
|
2992
|
+
if (wasStopped && !run.managed) {
|
|
2993
|
+
const link = await providerLinkState(root, provider);
|
|
2994
|
+
if (expectedRunId !== run.id || expectedRevision !== deliveryDecisionRevision(run) ||
|
|
2995
|
+
!link.connected || resumeObservation?.providerEpoch !== link.epoch ||
|
|
2996
|
+
resumeObservation?.session !== session || resumeObservation.cwd !== record.cwd ||
|
|
2997
|
+
resumeObservation.source !== 'codex_app_server' || !text(resumeObservation.model, 200) ||
|
|
2998
|
+
!resumeObservation.purpose?.ruleDigest || deliveryStopBlocksResume(run, now))
|
|
2999
|
+
throw new Error('Explicit resume requires the exact stopped run and current original provider observation.');
|
|
3000
|
+
record.admission = { source: 'codex_app_server', provider, session, cwd: record.cwd,
|
|
3001
|
+
model: resumeObservation.model, validatedAt: now,
|
|
3002
|
+
...(resumeObservation.nativeSandbox && resumeObservation.nativeSandboxSource?.type === 'sdk_rollout_turn_context' &&
|
|
3003
|
+
resumeObservation.nativeSandboxSource.session === session && resumeObservation.nativeSandboxSource.cwd === record.cwd
|
|
3004
|
+
? { nativeSandbox: structuredClone(resumeObservation.nativeSandbox), nativeSandboxSource: structuredClone(resumeObservation.nativeSandboxSource) } : {}) };
|
|
3005
|
+
if (link.consentEpoch) record.providerConsentEpoch = link.consentEpoch;
|
|
3006
|
+
const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] },
|
|
3007
|
+
'Read the original conversation and latest corrections, reconcile prior effects, and reconstruct remaining authorized work. Do not replay prior actions.');
|
|
3008
|
+
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced',
|
|
3009
|
+
approvalScopes: ['task'], approvalPolicy: 'wait', initialPlan: plan, initialAction: plan.alternatives[0].action,
|
|
3010
|
+
autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: resumeObservation.model,
|
|
3011
|
+
purpose: resumeObservation.purpose }, session, provider, record.process, record.admission, now);
|
|
3012
|
+
run.managed.purpose.previousCheckpoint = run.checkpoint;
|
|
3013
|
+
}
|
|
2982
3014
|
if (run.managed?.dependencyWait) throw new Error('Confirm the exact external dependency condition before resuming.');
|
|
2983
3015
|
if (run.managed?.phase === 'needs_clarification') throw new Error('Answer the current session question before resuming.');
|
|
2984
3016
|
if (!['blocked', 'recovery_required', 'paused'].includes(run.state) && !wasStopped && !wasVerifying) throw new Error('Only a blocked, paused, recoverable, unowned pending-verification task, or stopped managed app-server loop can resume.');
|
|
@@ -5,8 +5,15 @@ import { resolve, relative, isAbsolute, join } from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
// Completion reviews bind to the actual local state. No resets, stashes, writes,
|
|
7
7
|
// shell commands from a worker, or special verifier registration are needed.
|
|
8
|
-
export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256
|
|
8
|
+
export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, excludedPaths = [], inventoryLimit = 256,
|
|
9
|
+
maxEntries = Infinity, maxBytes = Infinity, deadline = Infinity } = {}) {
|
|
9
10
|
signal?.throwIfAborted();
|
|
11
|
+
let entries = 0, bytes = 0;
|
|
12
|
+
const checkBudget = () => {
|
|
13
|
+
signal?.throwIfAborted();
|
|
14
|
+
if (entries > maxEntries || bytes > maxBytes || Date.now() >= deadline)
|
|
15
|
+
throw new Error('Optional workspace inventory exceeded its preparation budget; relevant files remain available for direct inspection.');
|
|
16
|
+
};
|
|
10
17
|
const hash = createHash('sha256');
|
|
11
18
|
const paths = [];
|
|
12
19
|
const capture = entry => {
|
|
@@ -20,12 +27,13 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
|
|
|
20
27
|
const info = await lstat(path); versions.set(path, info); return info;
|
|
21
28
|
};
|
|
22
29
|
const walk = async (directory, depth = 0) => {
|
|
23
|
-
|
|
30
|
+
checkBudget();
|
|
24
31
|
if (depth > 30) throw new Error('Workspace review inventory exceeds its bound.');
|
|
25
32
|
await remember(directory);
|
|
26
33
|
for await (const entry of await opendir(directory)) {
|
|
27
34
|
signal?.throwIfAborted();
|
|
28
35
|
if (['.git', 'node_modules', '.npm-cache'].includes(entry.name)) continue;
|
|
36
|
+
entries++; checkBudget();
|
|
29
37
|
const path = join(directory, entry.name);
|
|
30
38
|
if (stateRoot && resolve(path) === resolve(stateRoot)) continue;
|
|
31
39
|
if (entry.isDirectory()) await walk(path, depth + 1);
|
|
@@ -36,7 +44,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
|
|
|
36
44
|
|
|
37
45
|
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
38
46
|
for (const path of paths) {
|
|
39
|
-
|
|
47
|
+
checkBudget();
|
|
40
48
|
const full = resolve(cwd, path), rel = relative(cwd, full);
|
|
41
49
|
if (rel.startsWith('..') || isAbsolute(rel)) throw new Error('Invalid workspace review path.');
|
|
42
50
|
if (stateRoot && (full === resolve(stateRoot) || full.startsWith(resolve(stateRoot) + '/'))) continue;
|
|
@@ -57,6 +65,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
|
|
|
57
65
|
continue;
|
|
58
66
|
}
|
|
59
67
|
if (!info.isFile()) { const identity=`nonregular:${info.mode}:${info.mtimeMs}`;hash.update(identity);capture({path,mode:info.mode,type:'nonregular',digest:createHash('sha256').update(identity).digest('hex')});continue; }
|
|
68
|
+
bytes += info.size; checkBudget();
|
|
60
69
|
const actual = await realpath(full), actualRel = relative(cwd, actual);
|
|
61
70
|
if (actualRel.startsWith('..') || isAbsolute(actualRel)) throw new Error('Workspace changed to an external link during review.');
|
|
62
71
|
// Stream large artifacts with fixed memory. Verify the opened descriptor
|
|
@@ -69,7 +78,7 @@ export async function workspaceIdentity(cwd, stateRoot, inventory, { signal, exc
|
|
|
69
78
|
hash.update(String(info.mode));
|
|
70
79
|
let position = 0;
|
|
71
80
|
while (position < before.size) {
|
|
72
|
-
|
|
81
|
+
checkBudget();
|
|
73
82
|
const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, before.size - position), position);
|
|
74
83
|
if (!bytesRead) throw new Error('Workspace artifact was truncated during hashing.');
|
|
75
84
|
const chunk = buffer.subarray(0, bytesRead);
|
|
@@ -106,7 +106,7 @@ export async function deliveryHookStatus({ root = rootDirectory(), now = Date.no
|
|
|
106
106
|
canPause: Boolean(r.run.managed) && (['active', 'auto_resuming'].includes(r.run.state) ||
|
|
107
107
|
r.run.state === 'recovery_required' && ['session_supervision', 'approved_action'].includes(r.run.recoveryCause) &&
|
|
108
108
|
r.run.managed.resumeChannel === 'codex_app_server' && r.run.managed.autoRecovery),
|
|
109
|
-
canResume: (['blocked', 'recovery_required', 'paused'].includes(r.run.state) || canExplicitlyResumeStopped(r.run) || canResumePendingVerification(r.run)) && r.run.managed?.phase !== 'needs_clarification' &&
|
|
109
|
+
canResume: (['blocked', 'recovery_required', 'paused'].includes(r.run.state) || canExplicitlyResumeStopped(r.run, { standard: r.provider === 'codex' }) || canResumePendingVerification(r.run)) && r.run.managed?.phase !== 'needs_clarification' &&
|
|
110
110
|
!r.run.managed?.pendingApproval && !r.run.managed?.controllerDecisionWait && !r.run.managed?.dependencyWait && !['pending', 'pending_received', 'transferred'].includes(r.run.managed?.handoff?.status) &&
|
|
111
111
|
!deliveryStopBlocksResume(r.run, now),
|
|
112
112
|
canApprove: r.run.state === 'blocked' && Boolean(r.run.managed?.pendingApproval) && !r.run.managed.pendingApproval.denied,
|
package/src/desktop-access.js
CHANGED
|
@@ -9,7 +9,7 @@ export function validDesktopAccessPath(path, slug) {
|
|
|
9
9
|
export const desktopAccessReady = value => value?.state === 'ready';
|
|
10
10
|
const recoveryActions = new Set(['refresh', 'reconnect', 'login', 'authorization_open', 'authorization_copy', 'organization_select', 'organization_cancel', 'organizations_refresh',
|
|
11
11
|
'check_update', 'auto_update_set_enabled', 'update', 'prepare_update_restart', 'quit', 'task_stop', 'delivery_stop', 'delivery_pause', 'delivery_reject',
|
|
12
|
-
'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members']);
|
|
12
|
+
'provider_disconnect', 'desktop_access_refresh', 'desktop_access_settings', 'desktop_access_members', 'desktop_sqm_settings']);
|
|
13
13
|
export function desktopActionAllowed(access, command) {
|
|
14
14
|
return desktopAccessReady(access) || recoveryActions.has(command?.action) ||
|
|
15
15
|
command?.action === 'session_set_auto_continue' && command.enabled === false ||
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Status snapshots are replaceable; command receipts and stop notifications are
|
|
2
|
+
// not. Never enqueue periodic snapshots behind a native reader that has paused.
|
|
3
|
+
// Control frames retain the stream's FIFO order and never wait for GUI drain.
|
|
4
|
+
export function createDesktopFrameWriter(output) {
|
|
5
|
+
let blocked = false, pendingStatus = null, statusClosed = false;
|
|
6
|
+
const drain = () => {
|
|
7
|
+
blocked = false;
|
|
8
|
+
const frame = pendingStatus;
|
|
9
|
+
pendingStatus = null;
|
|
10
|
+
if (!statusClosed && frame !== null) write(frame, true);
|
|
11
|
+
};
|
|
12
|
+
const write = (frame, isStatus = false) => {
|
|
13
|
+
if (isStatus && statusClosed) return;
|
|
14
|
+
if (isStatus && blocked) { pendingStatus = frame; return; }
|
|
15
|
+
// A control frame must not be followed by a snapshot captured before it.
|
|
16
|
+
// The next authorized projection will publish fresh state.
|
|
17
|
+
if (!isStatus) pendingStatus = null;
|
|
18
|
+
const accepted = output.write(frame);
|
|
19
|
+
if (accepted === false && !blocked && !statusClosed) {
|
|
20
|
+
blocked = true;
|
|
21
|
+
output.once('drain', drain);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return { write, close() {
|
|
25
|
+
statusClosed = true;
|
|
26
|
+
pendingStatus = null;
|
|
27
|
+
output.removeListener('drain', drain);
|
|
28
|
+
} };
|
|
29
|
+
}
|