@awak-app/simy-cli 0.5.24 → 0.5.25
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/package.json +1 -1
- package/src/agent.js +102 -22
- package/src/cli-contract.js +1 -1
- package/src/desktop-host.js +5 -0
- package/src/desktop-status.js +73 -13
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -141,6 +141,8 @@ export async function startAgent({
|
|
|
141
141
|
webOrigin = null,
|
|
142
142
|
sessionRoot,
|
|
143
143
|
updateManager = null,
|
|
144
|
+
deferStartupSynchronization = false,
|
|
145
|
+
onDesktopStatusChange = null,
|
|
144
146
|
dependencies = {},
|
|
145
147
|
quiet = false,
|
|
146
148
|
} = {}) {
|
|
@@ -188,7 +190,10 @@ export async function startAgent({
|
|
|
188
190
|
: [],
|
|
189
191
|
);
|
|
190
192
|
const installMode = updateManager?.snapshot?.()?.install_mode ?? null;
|
|
191
|
-
let contractSourceCapabilities = daemonCapabilities(
|
|
193
|
+
let contractSourceCapabilities = daemonCapabilities(
|
|
194
|
+
deferStartupSynchronization ? unverifiedCapabilities() : await readCapabilities(dependencies),
|
|
195
|
+
daemon,
|
|
196
|
+
);
|
|
192
197
|
let availableCapabilities;
|
|
193
198
|
let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
|
|
194
199
|
const refreshAvailableCapabilities = () => {
|
|
@@ -212,6 +217,11 @@ export async function startAgent({
|
|
|
212
217
|
let port = requestedPort;
|
|
213
218
|
let heartbeatTimer = null;
|
|
214
219
|
let heartbeatInFlight = null;
|
|
220
|
+
let shuttingDown = false;
|
|
221
|
+
const notifyDesktopStatusChange = () => {
|
|
222
|
+
if (shuttingDown) return;
|
|
223
|
+
try { onDesktopStatusChange?.(); } catch { /* Desktop status polling remains available. */ }
|
|
224
|
+
};
|
|
215
225
|
let durableLocalTaskWorker = null;
|
|
216
226
|
let sqmLocalReviewWorker = null;
|
|
217
227
|
let loginUrl = null;
|
|
@@ -226,19 +236,31 @@ export async function startAgent({
|
|
|
226
236
|
now: dependencies.statusNowMs,
|
|
227
237
|
});
|
|
228
238
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
let providerReadInFlight = null;
|
|
240
|
+
const readProviderStatuses = ({ force = false } = {}) => {
|
|
241
|
+
if (providerReadInFlight) return providerReadInFlight;
|
|
242
|
+
providerReadInFlight = (async () => {
|
|
243
|
+
const previous = providerStatuses;
|
|
244
|
+
await Promise.all(["codex", "claude"].map(async (provider) => {
|
|
245
|
+
const next = await (provider === "codex" ? codexStatus : claudeStatus)({ force });
|
|
246
|
+
// Publish each verified result independently. A slow Claude probe must
|
|
247
|
+
// not keep a completed Codex result hidden (or vice versa).
|
|
248
|
+
providerStatuses = { ...providerStatuses, [provider]: next };
|
|
249
|
+
refreshAvailableCapabilities();
|
|
250
|
+
if (previous[provider]?.version !== next.version || previous[provider]?.state !== next.state) {
|
|
251
|
+
notifyDesktopStatusChange();
|
|
252
|
+
}
|
|
253
|
+
}));
|
|
254
|
+
const binaryStateChanged = ["codex", "claude"].some((provider) =>
|
|
255
|
+
previous[provider]?.version !== providerStatuses[provider]?.version ||
|
|
256
|
+
previous[provider]?.state !== providerStatuses[provider]?.state);
|
|
257
|
+
if (!shuttingDown && (force || binaryStateChanged)) {
|
|
258
|
+
contractSourceCapabilities = daemonCapabilities(await readCapabilities(dependencies), daemon);
|
|
259
|
+
refreshAvailableCapabilities();
|
|
260
|
+
}
|
|
261
|
+
return providerStatuses;
|
|
262
|
+
})().finally(() => { providerReadInFlight = null; });
|
|
263
|
+
return providerReadInFlight;
|
|
242
264
|
};
|
|
243
265
|
|
|
244
266
|
const inspectReadyBackend = async (backend, options) => {
|
|
@@ -275,7 +297,7 @@ export async function startAgent({
|
|
|
275
297
|
};
|
|
276
298
|
|
|
277
299
|
const restoreRemoteRuns = async () => {
|
|
278
|
-
if (!isSessionValid(session, Date.now(), apiOrigin) || !session?.device_id) return;
|
|
300
|
+
if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin) || !session?.device_id) return;
|
|
279
301
|
const snapshots = dependencies.listRemoteRuns
|
|
280
302
|
? await dependencies.listRemoteRuns({
|
|
281
303
|
apiOrigin,
|
|
@@ -287,7 +309,9 @@ export async function startAgent({
|
|
|
287
309
|
apiBaseUrl: session.api_base_url,
|
|
288
310
|
token: session.token,
|
|
289
311
|
});
|
|
312
|
+
let restored = false;
|
|
290
313
|
for (const snapshot of snapshots) {
|
|
314
|
+
if (shuttingDown) return;
|
|
291
315
|
if (!snapshot?.id || registry.has(snapshot.id)) continue;
|
|
292
316
|
const repository = findRepository(repositoryInventory, snapshot.charter?.repository);
|
|
293
317
|
const restoredRun = restoreRun({
|
|
@@ -297,14 +321,17 @@ export async function startAgent({
|
|
|
297
321
|
localPath: repository?.local_path || null,
|
|
298
322
|
});
|
|
299
323
|
registry.create(restoredRun);
|
|
324
|
+
restored = true;
|
|
300
325
|
await publishRestoredLocalCodingRun(restoredRun);
|
|
301
326
|
}
|
|
327
|
+
if (restored) notifyDesktopStatusChange();
|
|
302
328
|
};
|
|
303
329
|
|
|
304
330
|
const heartbeat = () => {
|
|
305
|
-
if (!isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve(false);
|
|
331
|
+
if (shuttingDown || !isSessionValid(session, Date.now(), apiOrigin)) return Promise.resolve(false);
|
|
306
332
|
if (heartbeatInFlight) return heartbeatInFlight;
|
|
307
333
|
connectionStatus.attempt();
|
|
334
|
+
notifyDesktopStatusChange();
|
|
308
335
|
let deviceHeartbeatSucceeded = false;
|
|
309
336
|
const advertisedProviders = JSON.stringify(providerStatuses);
|
|
310
337
|
heartbeatInFlight = Promise.resolve()
|
|
@@ -319,24 +346,36 @@ export async function startAgent({
|
|
|
319
346
|
}),
|
|
320
347
|
)
|
|
321
348
|
.then(async () => {
|
|
349
|
+
if (shuttingDown) return false;
|
|
322
350
|
deviceHeartbeatSucceeded = true;
|
|
323
351
|
connectionStatus.success();
|
|
352
|
+
notifyDesktopStatusChange();
|
|
324
353
|
// Register the device immediately; provider subprocesses must not delay
|
|
325
354
|
// browser authorization. Publish verified readiness after their probe.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
355
|
+
const publishVerifiedProviders = async () => {
|
|
356
|
+
await readProviderStatuses();
|
|
357
|
+
if (!shuttingDown && JSON.stringify(providerStatuses) !== advertisedProviders) {
|
|
358
|
+
await heartbeatDevice({ apiOrigin, apiBaseUrl: session.api_base_url, token: session.token,
|
|
359
|
+
port, capabilities: refreshAvailableCapabilities(), repoInventory: repositoryInventory });
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
if (deferStartupSynchronization) {
|
|
363
|
+
void publishVerifiedProviders().catch((error) => reportHeartbeatError(error, quiet));
|
|
364
|
+
} else {
|
|
365
|
+
await publishVerifiedProviders();
|
|
330
366
|
}
|
|
367
|
+
if (shuttingDown) return false;
|
|
331
368
|
const sqmResult = sqmLocalReviewWorker
|
|
332
369
|
? await sqmLocalReviewWorker.poll()
|
|
333
370
|
: null;
|
|
371
|
+
if (shuttingDown) return false;
|
|
334
372
|
if (!sqmResult && durableLocalTaskWorker) await durableLocalTaskWorker.poll();
|
|
335
373
|
return true;
|
|
336
374
|
})
|
|
337
375
|
.catch((error) => {
|
|
338
376
|
if (!deviceHeartbeatSucceeded) {
|
|
339
377
|
const state = connectionStatus.failure(error);
|
|
378
|
+
notifyDesktopStatusChange();
|
|
340
379
|
if (state === "unauthorized") {
|
|
341
380
|
session = null;
|
|
342
381
|
loginUrl = null;
|
|
@@ -1634,20 +1673,34 @@ export async function startAgent({
|
|
|
1634
1673
|
}
|
|
1635
1674
|
}
|
|
1636
1675
|
|
|
1637
|
-
|
|
1676
|
+
const synchronizeStartup = async () => {
|
|
1638
1677
|
try {
|
|
1639
1678
|
await refreshAuthorizedRepositoryInventory();
|
|
1679
|
+
if (shuttingDown) return;
|
|
1640
1680
|
await heartbeat();
|
|
1681
|
+
if (shuttingDown) return;
|
|
1641
1682
|
await restoreRemoteRuns();
|
|
1642
1683
|
} catch (error) {
|
|
1643
1684
|
reportStartupConnectionError(error, quiet);
|
|
1644
1685
|
}
|
|
1686
|
+
};
|
|
1687
|
+
if (deferStartupSynchronization) {
|
|
1688
|
+
// The desktop can show restored tasks and account state while network and
|
|
1689
|
+
// provider checks run. Unknown remains unknown until each probe succeeds.
|
|
1690
|
+
void readProviderStatuses().catch((error) => reportStartupConnectionError(error, quiet));
|
|
1691
|
+
if (isSessionValid(session, Date.now(), apiOrigin)) {
|
|
1692
|
+
startHeartbeatTimer();
|
|
1693
|
+
void synchronizeStartup();
|
|
1694
|
+
}
|
|
1695
|
+
} else if (isSessionValid(session, Date.now(), apiOrigin)) {
|
|
1696
|
+
await synchronizeStartup();
|
|
1645
1697
|
startHeartbeatTimer();
|
|
1646
1698
|
}
|
|
1647
1699
|
|
|
1648
1700
|
let shutdownPromise = null;
|
|
1649
1701
|
const shutdown = () => {
|
|
1650
1702
|
if (shutdownPromise) return shutdownPromise;
|
|
1703
|
+
shuttingDown = true;
|
|
1651
1704
|
shutdownPromise = (async () => {
|
|
1652
1705
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1653
1706
|
updateManager?.stop?.();
|
|
@@ -1670,7 +1723,14 @@ export async function startAgent({
|
|
|
1670
1723
|
});
|
|
1671
1724
|
const getDesktopStatus = async () => {
|
|
1672
1725
|
const auth = desktopAuthStatus(session, { webOrigin: apiOrigin });
|
|
1673
|
-
|
|
1726
|
+
if (deferStartupSynchronization && !shuttingDown) {
|
|
1727
|
+
// Cached checks return immediately; expired checks refresh in the
|
|
1728
|
+
// background without making the desktop frame wait for subprocesses.
|
|
1729
|
+
void readProviderStatuses().catch((error) => reportStartupConnectionError(error, quiet));
|
|
1730
|
+
}
|
|
1731
|
+
const { codex, claude } = deferStartupSynchronization
|
|
1732
|
+
? providerStatuses
|
|
1733
|
+
: await readProviderStatuses();
|
|
1674
1734
|
return {
|
|
1675
1735
|
schemaVersion: 1,
|
|
1676
1736
|
sampledAt: new Date().toISOString(),
|
|
@@ -1995,6 +2055,26 @@ async function capabilities() {
|
|
|
1995
2055
|
inspectDesktopExecutor("codex"),
|
|
1996
2056
|
inspectDesktopExecutor("claude"),
|
|
1997
2057
|
]);
|
|
2058
|
+
return capabilitiesFromInspections(codex, claude);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function unverifiedCapabilities() {
|
|
2062
|
+
const unknown = (backend) => ({
|
|
2063
|
+
backend,
|
|
2064
|
+
execution_target: DESKTOP_EXECUTION_TARGET,
|
|
2065
|
+
execution_label: DESKTOP_EXECUTION_LABEL,
|
|
2066
|
+
provider_label: BACKEND_VERSION_POLICIES[backend].label,
|
|
2067
|
+
status: "checking",
|
|
2068
|
+
available: false,
|
|
2069
|
+
compatible: false,
|
|
2070
|
+
installed_version: null,
|
|
2071
|
+
minimum_version: BACKEND_VERSION_POLICIES[backend].minimumVersion,
|
|
2072
|
+
update_command: BACKEND_VERSION_POLICIES[backend].updateCommand,
|
|
2073
|
+
});
|
|
2074
|
+
return capabilitiesFromInspections(unknown("codex"), unknown("claude"));
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function capabilitiesFromInspections(codex, claude) {
|
|
1998
2078
|
return {
|
|
1999
2079
|
backends: { codex: codex.compatible, claude: claude.compatible },
|
|
2000
2080
|
backend_versions: {
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.25";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-host.js
CHANGED
|
@@ -78,6 +78,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
78
78
|
});
|
|
79
79
|
agent = await (dependencies.startAgent || startAgent)({
|
|
80
80
|
daemon: true, webOrigin, sessionRoot: root, quiet: true, updateManager: updater,
|
|
81
|
+
deferStartupSynchronization: true,
|
|
82
|
+
onDesktopStatusChange: () => {
|
|
83
|
+
// Let an in-progress frame finish before publishing its new snapshot.
|
|
84
|
+
setImmediate(() => { void publish().catch(() => {}); });
|
|
85
|
+
},
|
|
81
86
|
});
|
|
82
87
|
stopWorkflows = (dependencies.startWorkflowUpdates || startWorkflowUpdates)({ enabled: env.SIMY_AUTO_UPDATE !== "0" });
|
|
83
88
|
updater.on?.("change", () => { void publish().catch(() => {}); });
|
package/src/desktop-status.js
CHANGED
|
@@ -93,14 +93,18 @@ export function desktopTaskDetail(registries, key) {
|
|
|
93
93
|
if (!task) return null;
|
|
94
94
|
const attempt = task.snapshot?.attempts?.at(-1);
|
|
95
95
|
const request = task.request?.requirement || task.display_instruction || task.instruction || task.snapshot?.charter?.requirement;
|
|
96
|
+
const purpose = purposePresentation(request);
|
|
96
97
|
const result = typeof task.result === "string" ? task.result : task.result?.summary || attempt?.summary;
|
|
98
|
+
const report = reportPresentation(task.result) || reportPresentation(attempt?.raw_output?.agent_report, true) || reportPresentation(result);
|
|
97
99
|
const links = [];
|
|
98
100
|
const pr = safeDesktopLink(attempt?.pr_url);
|
|
99
101
|
if (pr) links.push({ label: "Pull request", url: pr });
|
|
100
102
|
return {
|
|
101
103
|
key,
|
|
102
|
-
instruction: desktopContent(request),
|
|
103
|
-
|
|
104
|
+
instruction: desktopContent(purpose?.purpose || request),
|
|
105
|
+
...(purpose ? { purpose, technicalInstruction: desktopContent(request) } : {}),
|
|
106
|
+
result: desktopContent(report?.summary || result),
|
|
107
|
+
...(report ? { report, ...(typeof result === "string" && result.trimStart().startsWith("{") ? { technicalResult: desktopContent(result) } : {}) } : {}),
|
|
104
108
|
error: desktopContent(task.error, 1000),
|
|
105
109
|
truncated: [request, result].some((value) => typeof value === "string" && value.length > 16000),
|
|
106
110
|
links,
|
|
@@ -224,26 +228,82 @@ function taskStatusItem(task, registryIndex) {
|
|
|
224
228
|
),
|
|
225
229
|
48,
|
|
226
230
|
);
|
|
231
|
+
const request = task?.request?.requirement || task?.display_instruction || task?.instruction ||
|
|
232
|
+
task?.snapshot?.charter?.requirement;
|
|
233
|
+
const label = purposePresentation(request)?.purpose || request || task?.id || "Task";
|
|
227
234
|
return {
|
|
228
235
|
key: `${registryIndex}:${bounded(task?.id || "", 240)}`,
|
|
229
236
|
id: bounded(String(task?.id || `unknown-${registryIndex}`), MAX_LABEL_LENGTH),
|
|
230
|
-
label: bounded(
|
|
231
|
-
String(
|
|
232
|
-
task?.request?.requirement ||
|
|
233
|
-
task?.display_instruction ||
|
|
234
|
-
task?.instruction ||
|
|
235
|
-
task?.snapshot?.charter?.requirement ||
|
|
236
|
-
task?.id ||
|
|
237
|
-
"Task",
|
|
238
|
-
),
|
|
239
|
-
MAX_LABEL_LENGTH,
|
|
240
|
-
),
|
|
237
|
+
label: bounded(String(label), MAX_LABEL_LENGTH),
|
|
241
238
|
state,
|
|
242
239
|
updatedAt:
|
|
243
240
|
Date.parse(task?.updated_at || task?.snapshot?.updated_at || task?.startedAt || 0) || 0,
|
|
244
241
|
};
|
|
245
242
|
}
|
|
246
243
|
|
|
244
|
+
// The Purpose-driven executor embeds a user-facing charter inside a generated
|
|
245
|
+
// instruction. Project only known charter fields; keep the envelope available
|
|
246
|
+
// on selection so no original context is lost or reinterpreted as progress.
|
|
247
|
+
function purposePresentation(request) {
|
|
248
|
+
if (typeof request !== "string" || request.length > 262_144 ||
|
|
249
|
+
!(request.includes("Purpose Charter and constraints (authoritative context):") ||
|
|
250
|
+
request.includes("Purpose Charter, hypotheses, verification order, and completion criteria (authoritative context):")) ||
|
|
251
|
+
!request.includes("You are the executor, not the conductor.")) return null;
|
|
252
|
+
const marker = "\nPurpose Charter:\n";
|
|
253
|
+
const start = request.indexOf(marker);
|
|
254
|
+
if (start < 0) return null;
|
|
255
|
+
const jsonStart = request.indexOf("{", start + marker.length);
|
|
256
|
+
if (jsonStart < 0) return null;
|
|
257
|
+
let depth = 0, quoted = false, escaped = false, end = -1;
|
|
258
|
+
for (let i = jsonStart; i < request.length; i += 1) {
|
|
259
|
+
const char = request[i];
|
|
260
|
+
if (escaped) { escaped = false; continue; }
|
|
261
|
+
if (quoted && char === "\\") { escaped = true; continue; }
|
|
262
|
+
if (char === '"') { quoted = !quoted; continue; }
|
|
263
|
+
if (quoted) continue;
|
|
264
|
+
if (char === "{") depth += 1;
|
|
265
|
+
if (char === "}") {
|
|
266
|
+
depth -= 1;
|
|
267
|
+
if (depth === 0) { end = i + 1; break; }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (end < 0) return null;
|
|
271
|
+
try {
|
|
272
|
+
const charter = JSON.parse(request.slice(jsonStart, end));
|
|
273
|
+
const clean = (value, max = 2000) => typeof value === "string" ? desktopContent(value, max).trim() : "";
|
|
274
|
+
const purpose = clean(charter.original_request || charter.requirement || charter.purpose);
|
|
275
|
+
if (!purpose) return null;
|
|
276
|
+
return {
|
|
277
|
+
purpose,
|
|
278
|
+
persona: clean(charter.persona),
|
|
279
|
+
expectedValue: clean(charter.expected_value),
|
|
280
|
+
successCriteria: Array.isArray(charter.success_criteria)
|
|
281
|
+
? charter.success_criteria.filter((value) => typeof value === "string").slice(0, 20).map((value) => clean(value, 1000)).filter(Boolean)
|
|
282
|
+
: [],
|
|
283
|
+
};
|
|
284
|
+
} catch { return null; }
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function reportPresentation(result, allowSchemaLess = false) {
|
|
288
|
+
if (typeof result !== "string" && (typeof result !== "object" || result === null)) return null;
|
|
289
|
+
try {
|
|
290
|
+
const value = typeof result === "string"
|
|
291
|
+
? result.length <= 65_536 && result.trimStart().startsWith("{") ? JSON.parse(result) : null
|
|
292
|
+
: result;
|
|
293
|
+
if ((value?.schema_version !== "agent_report.v1" && !(allowSchemaLess && !value?.schema_version)) || typeof value.summary !== "string") return null;
|
|
294
|
+
const list = (field) => Array.isArray(value[field])
|
|
295
|
+
? value[field].filter((item) => typeof item === "string").slice(0, 20).map((item) => desktopContent(item, 1000))
|
|
296
|
+
: [];
|
|
297
|
+
return {
|
|
298
|
+
summary: desktopContent(value.summary),
|
|
299
|
+
testsRun: list("tests_run"),
|
|
300
|
+
evidenceRefs: list("evidence_refs"),
|
|
301
|
+
unresolvedItems: list("unresolved_items"),
|
|
302
|
+
risks: list("risks"),
|
|
303
|
+
};
|
|
304
|
+
} catch { return null; }
|
|
305
|
+
}
|
|
306
|
+
|
|
247
307
|
function sessionUserLabel(session) {
|
|
248
308
|
if (!session) return null;
|
|
249
309
|
for (const key of ["user_label", "display_name", "email", "account_id", "auth_user_id"]) {
|