@awak-app/simy-cli 0.7.31 → 0.7.33
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/agent.js +7 -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 +27 -3
- 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 +176 -74
- 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 +39 -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 +101 -24
- package/src/session-supervision/owner-transport.js +23 -8
- package/src/session-supervision/progressive-discovery.js +66 -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/team-threads.js +3 -6
- package/src/update-connection.js +16 -0
- package/src/web-api.js +9 -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.33",
|
|
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/agent.js
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
webApiErrorMessage,
|
|
107
107
|
webApiHeaders,
|
|
108
108
|
webApiUrl,
|
|
109
|
+
publicImageUrl,
|
|
109
110
|
} from "./web-api.js";
|
|
110
111
|
import {
|
|
111
112
|
defaultRepositoryScanRoot,
|
|
@@ -672,9 +673,13 @@ export async function startAgent({
|
|
|
672
673
|
if (!Array.isArray(payload.organizations)) throw new Error("Invalid organization list.");
|
|
673
674
|
if (session !== requestedSession) return;
|
|
674
675
|
organizationItems = payload.organizations.filter(org => typeof org.id === "string" && typeof org.name === "string")
|
|
675
|
-
.slice(0, 100).map(org =>
|
|
676
|
+
.slice(0, 100).map(org => {
|
|
677
|
+
const logoUrl = publicImageUrl(org.logoUrl ?? org.logo_url);
|
|
678
|
+
return { id: org.id, name: org.name.slice(0, 120),
|
|
679
|
+
...(logoUrl ? { logoUrl } : {}),
|
|
676
680
|
...(["ready", "sqm_required", "developer_required", "unavailable"].includes(org.desktopAccess) ? { desktopAccess: org.desktopAccess } : {}),
|
|
677
|
-
...(typeof org.slug === 'string' && org.slug && org.slug.length <= 160 && !/[\/\\?#\x00-\x1f]/.test(org.slug) ? { slug: org.slug } : {}) }
|
|
681
|
+
...(typeof org.slug === 'string' && org.slug && org.slug.length <= 160 && !/[\/\\?#\x00-\x1f]/.test(org.slug) ? { slug: org.slug } : {}) };
|
|
682
|
+
});
|
|
678
683
|
organizationState = "ready";
|
|
679
684
|
} catch {
|
|
680
685
|
if (session === requestedSession) organizationState = "unavailable";
|
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.33";
|
|
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.33';
|
|
11
11
|
|
|
12
12
|
const STORE = 'cli-observability-state.json';
|
|
13
13
|
const LEGACY_CONFIG = 'cli-observability.json';
|
|
@@ -1115,7 +1115,7 @@ export async function enablePurposeDelivery({ root = rootDirectory(), provider,
|
|
|
1115
1115
|
if (managedBudgetReason(run, now)) throw new Error('Original budget exhausted; automatic continuation cannot reset it.');
|
|
1116
1116
|
if (!run.managed) {
|
|
1117
1117
|
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: '
|
|
1118
|
+
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced', approvalScopes: ['task'], approvalPolicy: 'wait',
|
|
1119
1119
|
initialPlan: plan, initialAction: plan.alternatives[0].action, autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: admission.model, purpose },
|
|
1120
1120
|
session, provider, record.process, admission, now);
|
|
1121
1121
|
run.managed.purpose.previousCheckpoint = run.checkpoint;
|
|
@@ -2089,7 +2089,9 @@ export function deliveryStopBlocksResume(run, now = Date.now()) {
|
|
|
2089
2089
|
}
|
|
2090
2090
|
// Stopped remains terminal for hooks, polling and automatic recovery. Only the
|
|
2091
2091
|
// explicit control action may reopen an enrolled app-server task for diagnosis.
|
|
2092
|
-
export function canExplicitlyResumeStopped(run) {
|
|
2092
|
+
export function canExplicitlyResumeStopped(run, { standard = false } = {}) {
|
|
2093
|
+
if (standard && run?.state === 'stopped' && !run.managed)
|
|
2094
|
+
return canUpgradeDelivery({ ...run, state: 'active' }) && (run.failures || 0) < run.maxFailures;
|
|
2093
2095
|
return run?.state === 'stopped' && run.managed?.resumeChannel === 'codex_app_server' &&
|
|
2094
2096
|
Boolean(run.managed.autonomyPolicy && run.managed.approvalScopes?.length) &&
|
|
2095
2097
|
!run.managed.resumeLease && !run.managed.liveWorkspaceLease;
|
|
@@ -2977,8 +2979,30 @@ export async function controlDelivery({ root = rootDirectory(), provider, sessio
|
|
|
2977
2979
|
run.providerRequest = null;
|
|
2978
2980
|
const marker = readState(stopMarkerPath(root, provider, session));
|
|
2979
2981
|
if (marker?.runId === run.id) run.handledStopMarkerId = marker.id;
|
|
2980
|
-
const wasStopped = canExplicitlyResumeStopped(run);
|
|
2982
|
+
const wasStopped = canExplicitlyResumeStopped(run, { standard: provider === 'codex' });
|
|
2981
2983
|
const wasVerifying = canResumePendingVerification(run);
|
|
2984
|
+
if (wasStopped && !run.managed) {
|
|
2985
|
+
const link = await providerLinkState(root, provider);
|
|
2986
|
+
if (expectedRunId !== run.id || expectedRevision !== deliveryDecisionRevision(run) ||
|
|
2987
|
+
!link.connected || resumeObservation?.providerEpoch !== link.epoch ||
|
|
2988
|
+
resumeObservation?.session !== session || resumeObservation.cwd !== record.cwd ||
|
|
2989
|
+
resumeObservation.source !== 'codex_app_server' || !text(resumeObservation.model, 200) ||
|
|
2990
|
+
!resumeObservation.purpose?.ruleDigest || deliveryStopBlocksResume(run, now))
|
|
2991
|
+
throw new Error('Explicit resume requires the exact stopped run and current original provider observation.');
|
|
2992
|
+
record.admission = { source: 'codex_app_server', provider, session, cwd: record.cwd,
|
|
2993
|
+
model: resumeObservation.model, validatedAt: now,
|
|
2994
|
+
...(resumeObservation.nativeSandbox && resumeObservation.nativeSandboxSource?.type === 'sdk_rollout_turn_context' &&
|
|
2995
|
+
resumeObservation.nativeSandboxSource.session === session && resumeObservation.nativeSandboxSource.cwd === record.cwd
|
|
2996
|
+
? { nativeSandbox: structuredClone(resumeObservation.nativeSandbox), nativeSandboxSource: structuredClone(resumeObservation.nativeSandboxSource) } : {}) };
|
|
2997
|
+
if (link.consentEpoch) record.providerConsentEpoch = link.consentEpoch;
|
|
2998
|
+
const plan = purposeDiagnosisPlan({ approvalScopes: ['task'] },
|
|
2999
|
+
'Read the original conversation and latest corrections, reconcile prior effects, and reconstruct remaining authorized work. Do not replay prior actions.');
|
|
3000
|
+
run.managed = managedSpec({ acceptanceCriteria: [], contextBootstrap: true, autonomyPolicy: 'balanced',
|
|
3001
|
+
approvalScopes: ['task'], approvalPolicy: 'wait', initialPlan: plan, initialAction: plan.alternatives[0].action,
|
|
3002
|
+
autoRecovery: true, resumeChannel: 'codex_app_server', resumeModel: resumeObservation.model,
|
|
3003
|
+
purpose: resumeObservation.purpose }, session, provider, record.process, record.admission, now);
|
|
3004
|
+
run.managed.purpose.previousCheckpoint = run.checkpoint;
|
|
3005
|
+
}
|
|
2982
3006
|
if (run.managed?.dependencyWait) throw new Error('Confirm the exact external dependency condition before resuming.');
|
|
2983
3007
|
if (run.managed?.phase === 'needs_clarification') throw new Error('Answer the current session question before resuming.');
|
|
2984
3008
|
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
|
+
}
|