@awak-app/simy-cli 0.6.13 → 0.6.17
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 +91 -6
- package/package.json +1 -1
- package/src/agent.js +97 -23
- package/src/auto-update.js +55 -6
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/connection-reminder.mjs +5 -2
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +30 -3
- package/src/codex-plugins/simy/hooks/dev-work.mjs +7 -1
- package/src/codex-plugins/simy/hooks/hooks.json +2 -2
- package/src/codex-work-pattern-sync.js +37 -9
- package/src/delivery-hooks/command.js +9 -2
- package/src/delivery-hooks/desktop.js +10 -12
- package/src/delivery-hooks/evidence.js +50 -0
- package/src/delivery-hooks/supervisor.js +8 -2
- package/src/desktop-access.js +5 -3
- package/src/desktop-host.js +511 -74
- package/src/desktop-update-stop.js +22 -0
- package/src/doctor.js +2 -1
- package/src/host-exit-diagnostics.js +28 -0
- package/src/index.js +1 -0
- package/src/provider-links.js +74 -0
- package/src/session-supervision/action-association-host.js +23 -10
- package/src/session-supervision/association-refresh.js +12 -0
- package/src/session-supervision/association-store.js +26 -10
- package/src/session-supervision/automatic-continuation.js +31 -2
- package/src/session-supervision/claude-sessions.js +25 -15
- package/src/session-supervision/codex-app-server.js +27 -4
- package/src/session-supervision/index.js +163 -42
- package/src/session-supervision/statistics.js +76 -15
- package/src/sqm/command.js +58 -7
- package/src/sqm/evidence-client.js +11 -1
- package/src/sqm/failure-circuit.js +28 -6
- package/src/sqm/history-store.js +49 -18
- package/src/sqm/provider-diagnostics.js +1 -0
- package/src/sqm/review-worker.js +70 -21
- package/src/sqm/workflow-update.js +12 -3
- package/src/windows-desktop-updater.js +6 -1
package/README.md
CHANGED
|
@@ -2,16 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
Local execution agent for SIMY Local Tasks and Agentic Loop runs.
|
|
4
4
|
|
|
5
|
+
Action candidate search runs once for each authenticated live conversation context,
|
|
6
|
+
and again when that context changes or **Refresh** is selected. Background binding
|
|
7
|
+
renewal uses the lightweight binding endpoint instead of repeating candidate search.
|
|
8
|
+
Settled ordinary conversations and terminal managed tasks do not automatically
|
|
9
|
+
search candidates. Bound Actions still renew to reflect changes to deadlines and
|
|
10
|
+
manual links. Candidate text expires after one minute; select **Refresh** to retrieve
|
|
11
|
+
current suggestions after editing Actions in SIMY. Candidate search identities are
|
|
12
|
+
cached in memory, so restarting the host may retrieve live contexts again.
|
|
13
|
+
|
|
5
14
|
`simy --version` prints the installed CLI version without starting an agent or
|
|
6
15
|
checking for updates. `simy doctor` performs read-only local connection and
|
|
7
16
|
provider login checks and prints a JSON diagnostic report: installed/running
|
|
8
17
|
versions, stale discovery records, session expiry, provider readiness, and recent
|
|
9
|
-
structured SQM failures
|
|
18
|
+
structured SQM failures, plus `host_exit` with the latest native desktop host's
|
|
19
|
+
abnormal exit time and status. `failures.total_failures` counts scoped SQM
|
|
20
|
+
failures only; zero does not mean the desktop has never crashed. `host_exit` is
|
|
21
|
+
local machine history, independent of account/organization and current daemon
|
|
22
|
+
reachability. A recorded exit remains visible after restart; `not_recorded` means
|
|
23
|
+
no retained exit record, and `unavailable` means it could not be verified. It is
|
|
24
|
+
not a lifetime crash count. Only the fixed header is read; stderr is never
|
|
25
|
+
included. Doctor does not run a model, renew login, or restart tasks.
|
|
10
26
|
Provider login is separate from model access; unverified model access remains
|
|
11
27
|
`unknown`. Diagnostic journals retain stage/code/HTTP status only, use private
|
|
12
28
|
files scoped to the Web/account/organization, rotate each writer's four 64 KiB
|
|
13
29
|
segments, and keep lifetime failure counts after event rotation.
|
|
14
30
|
|
|
31
|
+
The desktop host continues sampling connection and access while optional
|
|
32
|
+
conversation metadata is pending. It retains the last completed conversation
|
|
33
|
+
metadata only within the same verified scope, clears it after access or account
|
|
34
|
+
changes, and merges completed metadata with the newer connection observation.
|
|
35
|
+
This prevents a slow metadata read from making a live service appear disconnected;
|
|
36
|
+
it does not conceal actual host exits, failed connections, or revoked access.
|
|
37
|
+
|
|
15
38
|
The organization menu's **Allow Codex / Claude fallback** setting defaults to
|
|
16
39
|
off and grants only read-only SQM analysis permission to use the other signed-in
|
|
17
40
|
local provider. It is saved per Web/account/organization. Switching requires an
|
|
@@ -115,6 +138,12 @@ a fresh Codex or Claude login probe succeeds, including after an app restart.
|
|
|
115
138
|
An unchanged ready status cannot repeatedly re-run a failed check. Manual retry
|
|
116
139
|
remains available; continuing the conversation still requires **Auto-continue**.
|
|
117
140
|
|
|
141
|
+
You can enable **Auto-continue** while a conversation is running when its original
|
|
142
|
+
provider configuration is verified. SIMY saves the preference and waits for the
|
|
143
|
+
current turn to finish before checking remaining work. Configuration and runtime
|
|
144
|
+
blockers appear in the conversation view with recovery guidance; turning the
|
|
145
|
+
setting off does not interrupt the current turn.
|
|
146
|
+
|
|
118
147
|
Claude read-only structured reviews retry once when the provider explicitly
|
|
119
148
|
reports exhausted turns or structured-output repair attempts, within the original
|
|
120
149
|
timeout. Other failures retain distinct codes and recovery hints. Safe native
|
|
@@ -202,9 +231,31 @@ marketplace still points to SIMY’s generated local source. Plugin versions inc
|
|
|
202
231
|
receive a fresh cache. Use a new Codex task to pick up updated skills; running
|
|
203
232
|
tasks are not restarted. Missing or disabled plugins are not enabled automatically.
|
|
204
233
|
Failed refreshes retry on the next check without blocking daemon startup.
|
|
234
|
+
Auto-continue ON saves consent for the selected original conversation; it does not
|
|
235
|
+
immediately launch a second turn. A verified desktop discovery admission or an
|
|
236
|
+
existing exact admission saves this preference without another provider RPC.
|
|
237
|
+
After the current turn settles, continuation separately rechecks context, the
|
|
238
|
+
live provider channel, approvals and the opt-in epoch. Required human decisions
|
|
239
|
+
remain pending, and OFF prevents future continuation without interrupting the
|
|
240
|
+
current turn. The macOS switch and label explain these boundaries through their
|
|
241
|
+
tooltips and accessibility help.
|
|
242
|
+
|
|
243
|
+
Conversation statistics estimate work time from activity events within the last
|
|
244
|
+
seven days. Usage counters alone leave work time, idle time and utilization
|
|
245
|
+
unavailable; a single observed activity event is a measured zero-duration sample.
|
|
246
|
+
Missing usage records remain distinct from recorded zero tokens.
|
|
247
|
+
|
|
205
248
|
The desktop Sessions page loads a recent window of up to 200 conversations,
|
|
206
|
-
ranked by last activity.
|
|
207
|
-
|
|
249
|
+
ranked by last activity. Codex candidates come from CLI, VS Code and app-server
|
|
250
|
+
conversation sources; non-interactive exec, unknown sources and delegated agents
|
|
251
|
+
are excluded. A hook observation alone cannot restore a filtered conversation.
|
|
252
|
+
Explicitly tracked, monitored, auto-continue and unsettled supervision records
|
|
253
|
+
remain visible even outside that window. When a retained Codex conversation is
|
|
254
|
+
omitted from the recent provider page, SIMY reads its exact metadata without
|
|
255
|
+
loading turns or taking writer ownership. The same interactive source filter and
|
|
256
|
+
a matching active rollout path are required before restoring provider capabilities
|
|
257
|
+
and probing its latest turn. Exact reads and turn probes share a bounded
|
|
258
|
+
observation deadline. Claude discovery
|
|
208
259
|
ranks file metadata before opening selected transcripts. A conflicting saved
|
|
209
260
|
workspace leaves only that conversation's Action association unavailable; it
|
|
210
261
|
does not stop discovery or change the saved identity. The page lets users
|
|
@@ -577,6 +628,26 @@ execution. Keep concrete regression and delivery acceptance evidence separately.
|
|
|
577
628
|
If knowledge is unavailable, the command returns exit 2 with the service cause
|
|
578
629
|
and writes no new proof; any older artifact is not evidence for that attempt.
|
|
579
630
|
|
|
631
|
+
Cloud storage is separate from the check outcome. `check` retains the completed
|
|
632
|
+
proof and local check history if its evidence POST fails, returns exit 2 when
|
|
633
|
+
storage is unconfirmed (exit 1 still takes precedence for a failed check), and
|
|
634
|
+
writes a redacted storage receipt to `<proof-path>.upload.json`. Only a server
|
|
635
|
+
acknowledgement matching the proof ID, evidence ID and proof digest counts as
|
|
636
|
+
stored. `--no-upload` intentionally keeps evidence local and does not return
|
|
637
|
+
exit 2 for storage. A successful upload is not evidence of applicable coverage.
|
|
638
|
+
|
|
639
|
+
After resolving a storage/authentication failure, explicitly recover the original
|
|
640
|
+
run with `simy sqm upload-proof --proof /path/to/proof.json` (add
|
|
641
|
+
`--signed-evidence /path/to/signature.json` if using a custom signature path, and
|
|
642
|
+
`--host <original-origin>` for its SIMY environment). This verifies the original
|
|
643
|
+
signature and current organization/account/device before sending the same proof;
|
|
644
|
+
it never reruns checks, resigns evidence or automatically replays an ambiguous
|
|
645
|
+
POST. The backend reconciles repeated proof IDs against their exact digest and
|
|
646
|
+
evidence ID. The receipt includes the saved record ID and evidence ID on success.
|
|
647
|
+
This recovers a historical run, not acceptance of the current working tree.
|
|
648
|
+
Dev/production record visibility, deployed gateway/schema and UI destination
|
|
649
|
+
permissions still require validation in the corresponding environment.
|
|
650
|
+
|
|
580
651
|
Normal runs do not require a separate sync command: the CLI
|
|
581
652
|
uses a verified local cache and revalidates it with TTL and ETag semantics.
|
|
582
653
|
The Agentic Loop runs min-check after each code modification and the full check
|
|
@@ -688,7 +759,7 @@ from an empty CLI chat, types repository, branch, executor, attachment, and task
|
|
|
688
759
|
input through a pseudoterminal, creates the Web ledger, spawns real child
|
|
689
760
|
processes, and exercises HIL plus lifecycle controls. It regenerates the
|
|
690
761
|
screenshots, terminal frames, raw transcript, and SHA-256 manifest under
|
|
691
|
-
|
|
762
|
+
`.artifacts/e2e/cli-agentic-loop-console/`.
|
|
692
763
|
|
|
693
764
|
## Delivery Loop design proposals
|
|
694
765
|
|
|
@@ -806,11 +877,25 @@ explains baseline updates, historical regression coverage and evidence limits.
|
|
|
806
877
|
SQM Tasks has separate **Analysis** and **Checks** lists on macOS and Windows.
|
|
807
878
|
Analysis retains local review summaries; Checks retains both `simy sqm min-check`
|
|
808
879
|
and `simy sqm check` outcomes, including checks run by the Delivery Loop.
|
|
809
|
-
Each list
|
|
810
|
-
`~/.simy/sqm-history`)
|
|
880
|
+
Each desktop list displays the latest 100 runs. Durable summary files under
|
|
881
|
+
`SIMY_HOME/sqm-history` (default `~/.simy/sqm-history`) are retained without
|
|
882
|
+
a count or age cutoff, isolated by web origin, account and organization. Adding
|
|
883
|
+
a run never deletes older history. Old job keys remain available through the
|
|
884
|
+
local task-detail lookup, including failure stage/code, HTTP status, provider
|
|
885
|
+
diagnostics and the matching web task link. The separate rotating diagnostic
|
|
886
|
+
log is a recent-event view; retained job summaries provide longer-term correlation.
|
|
887
|
+
Operators should include this directory in their disk usage and backup policy.
|
|
888
|
+
Deleting a scope directory explicitly removes its history; previously pruned
|
|
889
|
+
records cannot be recovered.
|
|
811
890
|
History remains visible with local analysis off and after restarting. A saved
|
|
812
891
|
in-flight analysis is shown as Interrupted until this client claims it again;
|
|
813
892
|
it does not contribute to the running count. The web workspace owns retry status.
|
|
893
|
+
Model/provider execution failures permit one recovery claim after a five-minute
|
|
894
|
+
cooldown, even when the app/account identity is unchanged. If that attempt fails
|
|
895
|
+
again, the pause survives reconnects and host restarts until a newer SIMY version
|
|
896
|
+
is installed. Sign-in, local installation, SIMY access, schema and persistence
|
|
897
|
+
errors do not open this automatic provider recovery path. The existing six-failure
|
|
898
|
+
circuit still protects other analysis failures.
|
|
814
899
|
|
|
815
900
|
Only summary metadata is retained, without analysis prompts, source inputs,
|
|
816
901
|
model responses or credentials. Historical summaries are not a replacement for
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { normalizeProviderModelSelection } from "./provider-model-selection.js";
|
|
2
2
|
import { assertConnectionScope, connectionScope, connectionDiagnostic } from "./connection-diagnostics.js";
|
|
3
3
|
import { authorizeLaunch } from "./launch-authorization.js";
|
|
4
|
+
import { resolveDesktopExecutorCommand } from "./desktop-executor.js";
|
|
4
5
|
import { normalizePurposeVerification, assertPurposeActor, preparePurposeVerification, verifyPurposeLocalTask, purposeSigningKeyPath, purposeVerificationCapability, purposeWorkspaceBusy } from "./purpose-local-proof.js";
|
|
5
6
|
import { startupRequirementChecks, summarizeStartupChecks } from "./startup-preflight.js";
|
|
6
7
|
import { BROWSER_HANDOFF_TRANSPORT, browserHandoffPage, decryptBrowserHandoff } from "./browser-handoff.js";
|
|
7
8
|
import { createAuthorizationWindow } from "./browser-authorization.js";
|
|
8
9
|
import { createSqmHistoryStore, projectSqmHistory, sqmHistoryScope } from "./sqm/history-store.js";
|
|
9
10
|
import { createSqmFailureCircuit, createSqmFailureCircuitStore } from "./sqm/failure-circuit.js";
|
|
11
|
+
import { readProviderLinks } from './provider-links.js';
|
|
10
12
|
import { createDesktopAccessPolicy, desktopAccessReady, requireDesktopAccess } from './desktop-access.js';
|
|
11
13
|
import { readSqmEnabled, writeSqmEnabled, readSqmSettings, writeSqmEngineFallback } from "./desktop-sqm-settings.js";
|
|
12
14
|
import { createServer } from "node:http";
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
restoreRun,
|
|
36
38
|
resumeLocalCodingRun,
|
|
37
39
|
startLocalCodingRun,
|
|
40
|
+
executeProcessAttempt,
|
|
38
41
|
syncLocalCodingRun,
|
|
39
42
|
stopLocalCodingRun,
|
|
40
43
|
toCompatibleCodingLoopState,
|
|
@@ -269,6 +272,13 @@ export async function startAgent({
|
|
|
269
272
|
daemon,
|
|
270
273
|
);
|
|
271
274
|
let availableCapabilities;
|
|
275
|
+
const inspectedLinks = new WeakMap(), runAuthorizations = new WeakMap();
|
|
276
|
+
let providerLinks = await readProviderLinks(sessionRoot);
|
|
277
|
+
const requireLinkedProvider = async (provider, expected) => {
|
|
278
|
+
const link = (await readProviderLinks(sessionRoot))[provider];
|
|
279
|
+
if (!link?.connected || expected && link.epoch !== expected.epoch) throw Object.assign(new Error("Reconnect this provider to SIMY before using it."), { code: "provider_link_disconnected" });
|
|
280
|
+
return link;
|
|
281
|
+
};
|
|
272
282
|
let providerStatuses = { codex: { state: "unknown" }, claude: { state: "unknown" } };
|
|
273
283
|
const providerRecoveryRevisions = { codex: 0, claude: 0 };
|
|
274
284
|
const refreshAvailableCapabilities = () => {
|
|
@@ -390,6 +400,7 @@ export async function startAgent({
|
|
|
390
400
|
token: session.token,
|
|
391
401
|
deviceId: session.device_id,
|
|
392
402
|
accountId: session.account_id,
|
|
403
|
+
connectedProviders: ["codex", "claude"].filter(provider => providerLinks[provider]?.connected),
|
|
393
404
|
});
|
|
394
405
|
} catch (error) {
|
|
395
406
|
reportCodexWorkPatternSyncError(error, quiet);
|
|
@@ -449,7 +460,7 @@ export async function startAgent({
|
|
|
449
460
|
providerReadForced = force;
|
|
450
461
|
providerReadInFlight = (async () => {
|
|
451
462
|
const previous = providerStatuses;
|
|
452
|
-
await Promise.all(["codex", "claude"].map(async (provider) => {
|
|
463
|
+
await Promise.all([readProviderLinks(sessionRoot).then(links => { providerLinks = links; }), ...["codex", "claude"].map(async (provider) => {
|
|
453
464
|
const next = await (provider === "codex" ? codexStatus : claudeStatus)({ force });
|
|
454
465
|
// Publish each verified result independently. A slow Claude probe must
|
|
455
466
|
// not keep a completed Codex result hidden (or vice versa).
|
|
@@ -459,7 +470,7 @@ export async function startAgent({
|
|
|
459
470
|
providerRecoveryRevisions[provider] += 1;
|
|
460
471
|
notifyDesktopStatusChange();
|
|
461
472
|
}
|
|
462
|
-
}));
|
|
473
|
+
})]);
|
|
463
474
|
const binaryStateChanged = ["codex", "claude"].some((provider) =>
|
|
464
475
|
previous[provider]?.version !== providerStatuses[provider]?.version ||
|
|
465
476
|
previous[provider]?.state !== providerStatuses[provider]?.state);
|
|
@@ -473,12 +484,15 @@ export async function startAgent({
|
|
|
473
484
|
};
|
|
474
485
|
|
|
475
486
|
const inspectReadyBackend = async (backend, options) => {
|
|
487
|
+
const link = await requireLinkedProvider(backend);
|
|
476
488
|
await readProviderStatuses(options);
|
|
489
|
+
await requireLinkedProvider(backend, link);
|
|
477
490
|
const inspection = normalizeBackendInspection(backend, availableCapabilities);
|
|
478
491
|
if (inspection.compatible) {
|
|
479
492
|
const failure = providerAuthenticationFailure(backend, providerStatuses[backend]);
|
|
480
493
|
if (failure) throw Object.assign(new Error(failure.summary), failure);
|
|
481
494
|
}
|
|
495
|
+
inspectedLinks.set(inspection, link);
|
|
482
496
|
return inspection;
|
|
483
497
|
};
|
|
484
498
|
|
|
@@ -487,11 +501,53 @@ export async function startAgent({
|
|
|
487
501
|
const backend = run.request.backend;
|
|
488
502
|
const inspection = await inspectReadyBackend(backend, { force: true });
|
|
489
503
|
await authorizeDesktopWork(scope);
|
|
504
|
+
await requireLinkedProvider(backend, inspectedLinks.get(inspection));
|
|
490
505
|
if (!inspection.compatible) {
|
|
491
506
|
throw Object.assign(new Error(backendPreflightSummary(backend, inspection)), {
|
|
492
507
|
code: `desktop_executor_${inspection.status}`,
|
|
493
508
|
});
|
|
494
509
|
}
|
|
510
|
+
await bindRunAuthorization(run, scope, inspection);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const authorizeProviderDispatch = async (backend, scope, link) => {
|
|
514
|
+
await authorizeDesktopWork(scope);
|
|
515
|
+
await requireLinkedProvider(backend, link);
|
|
516
|
+
};
|
|
517
|
+
const bindRunAuthorization = async (run, scope, inspection) => {
|
|
518
|
+
const links = await readProviderLinks(sessionRoot);
|
|
519
|
+
links[run.request.backend] = inspectedLinks.get(inspection);
|
|
520
|
+
await authorizeProviderDispatch(run.request.backend, scope, links[run.request.backend]);
|
|
521
|
+
runAuthorizations.set(run, { scope, links });
|
|
522
|
+
};
|
|
523
|
+
const providerRuntimeOptions = (task, options = {}, scope, link) => {
|
|
524
|
+
const guard = () => authorizeProviderDispatch(task.backend, scope, link);
|
|
525
|
+
const wrap = callback => async (...args) => {
|
|
526
|
+
await guard();
|
|
527
|
+
await callback?.(...args);
|
|
528
|
+
await guard();
|
|
529
|
+
};
|
|
530
|
+
return { ...options, beforeProviderInvocation: wrap(options.beforeProviderInvocation), beforeSpawn: wrap(options.beforeSpawn) };
|
|
531
|
+
};
|
|
532
|
+
const codingRuntimeOptions = run => {
|
|
533
|
+
const wrap = (custom, marker) => async context => {
|
|
534
|
+
// Explicit resume may reauthorize the retained run callbacks. Freeze this
|
|
535
|
+
// invocation's scope so reconnection during command resolution cannot do so.
|
|
536
|
+
const authorization = runAuthorizations.get(run);
|
|
537
|
+
const guard = backend => authorizeProviderDispatch(backend, authorization?.scope, authorization?.links[backend]);
|
|
538
|
+
await guard(context.backend);
|
|
539
|
+
if (custom) return custom(context);
|
|
540
|
+
const repositoryPath = await resolveRepositoryPath(run.request);
|
|
541
|
+
return executeProcessAttempt({ ...context, run, repositoryPath, ...(marker ? { marker } : {}),
|
|
542
|
+
resolveCommandImpl: async input => {
|
|
543
|
+
const command = await (dependencies.resolveDesktopExecutorCommand || resolveDesktopExecutorCommand)(input);
|
|
544
|
+
await guard(input.backend);
|
|
545
|
+
return command;
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
return { ...runOptions, executeAttempt: wrap(runOptions.executeAttempt),
|
|
550
|
+
executeIndependentAudit: wrap(runOptions.executeIndependentAudit, "SIMY_AUDIT_JSON:") };
|
|
495
551
|
};
|
|
496
552
|
|
|
497
553
|
let organizationCancellation = Promise.resolve();
|
|
@@ -933,7 +989,9 @@ export async function startAgent({
|
|
|
933
989
|
});
|
|
934
990
|
}
|
|
935
991
|
await authorizeDesktopWork(scope);
|
|
936
|
-
|
|
992
|
+
const link = inspectedLinks.get(inspection);
|
|
993
|
+
await requireLinkedProvider(task.backend, link);
|
|
994
|
+
return (dependencies.runLocalTask || startLocalTask)(task, providerRuntimeOptions(task, options, scope, link));
|
|
937
995
|
},
|
|
938
996
|
attachmentStore: localTaskAttachmentStore,
|
|
939
997
|
getDeviceId: () => session?.device_id || null,
|
|
@@ -964,7 +1022,9 @@ export async function startAgent({
|
|
|
964
1022
|
const actor = JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]);
|
|
965
1023
|
const settings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
966
1024
|
const engineSettings = JSON.stringify([settings.enable_engine_fallback, settings.allowed_fallback_providers]);
|
|
967
|
-
const
|
|
1025
|
+
const reviewLinks = await readProviderLinks(sessionRoot);
|
|
1026
|
+
const beforeDispatch = async (selectedProvider = provider) => {
|
|
1027
|
+
await requireLinkedProvider(selectedProvider, reviewLinks[selectedProvider]);
|
|
968
1028
|
await authorizeDesktopWork(scope);
|
|
969
1029
|
const current = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
970
1030
|
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
@@ -973,19 +1033,22 @@ export async function startAgent({
|
|
|
973
1033
|
}
|
|
974
1034
|
await context.beforeLeaseDispatch?.();
|
|
975
1035
|
await authorizeDesktopWork(scope);
|
|
1036
|
+
await requireLinkedProvider(selectedProvider, reviewLinks[selectedProvider]);
|
|
976
1037
|
const finalSettings = await readSqmSettings(sessionRoot, apiOrigin, session);
|
|
977
1038
|
if (actor !== JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id]) ||
|
|
978
1039
|
engineSettings !== JSON.stringify([finalSettings.enable_engine_fallback, finalSettings.allowed_fallback_providers])) {
|
|
979
1040
|
throw Object.assign(new Error("SQM actor or fallback settings changed."), { code: "desktop_access_required" });
|
|
980
1041
|
}
|
|
1042
|
+
await requireLinkedProvider(selectedProvider, reviewLinks[selectedProvider]);
|
|
981
1043
|
};
|
|
982
1044
|
return (dependencies.runSqmLocalReview || ((value, selected, options) => runStructuredProviderReview(selected, value, options)))(request, provider, {
|
|
983
1045
|
signal: context.signal, beforeDispatch, readOnlyAnalysis: true, enableEngineFallback: settings.enable_engine_fallback,
|
|
1046
|
+
allowAutomaticRetry: context.allowAutomaticRetry, beforeAutomaticRetry: context.beforeAutomaticRetry,
|
|
984
1047
|
allowedFallbackProviders: settings.allowed_fallback_providers, providerReportingSupported: context.providerReportingSupported === true,
|
|
985
1048
|
});
|
|
986
1049
|
},
|
|
987
1050
|
getEnvironmentKey: () => JSON.stringify([apiOrigin, session?.account_id, session?.organization_id, session?.device_id,
|
|
988
|
-
session?.expires_at, sqmEnabled, sqmEngineSettings, ["codex", "claude"].map(provider => [providerStatuses[provider]?.state, providerStatuses[provider]?.version])]),
|
|
1051
|
+
session?.expires_at, sqmEnabled, sqmEngineSettings, providerLinks, ["codex", "claude"].map(provider => [providerStatuses[provider]?.state, providerStatuses[provider]?.version])]),
|
|
989
1052
|
onChange: notifyDesktopStatusChange,
|
|
990
1053
|
getWorkspaceUrl: () => {
|
|
991
1054
|
const slug = desktopAccess.snapshot().organizationSlug;
|
|
@@ -996,7 +1059,7 @@ export async function startAgent({
|
|
|
996
1059
|
recordHistory: (scope, item) => sqmHistory.record(scope, item),
|
|
997
1060
|
onHistoryError: () => { if (!quiet) console.error("Could not save the local SQM analysis history."); },
|
|
998
1061
|
getProvider: () => !desktopWorkAllowed() || availableCapabilities?.features?.sqm_local_review !== true ? null : ["codex", "claude"].find(
|
|
999
|
-
(provider) => availableCapabilities?.backends?.[provider] === true &&
|
|
1062
|
+
(provider) => providerLinks[provider]?.connected === true && availableCapabilities?.backends?.[provider] === true &&
|
|
1000
1063
|
providerStatuses[provider]?.state === "ready",
|
|
1001
1064
|
) || null,
|
|
1002
1065
|
onError: (error) => reportSqmLocalReviewError(error, quiet),
|
|
@@ -1025,7 +1088,7 @@ export async function startAgent({
|
|
|
1025
1088
|
continueLocalCodingRunAfterRepositoryApproval(
|
|
1026
1089
|
run,
|
|
1027
1090
|
{ repository: selected.repository, localPath: selected.local_path },
|
|
1028
|
-
|
|
1091
|
+
codingRuntimeOptions(run),
|
|
1029
1092
|
),
|
|
1030
1093
|
);
|
|
1031
1094
|
};
|
|
@@ -1156,6 +1219,7 @@ export async function startAgent({
|
|
|
1156
1219
|
return;
|
|
1157
1220
|
}
|
|
1158
1221
|
try {
|
|
1222
|
+
await requireLinkedProvider(event.source === "claude_code" ? "claude" : "codex");
|
|
1159
1223
|
json(
|
|
1160
1224
|
res,
|
|
1161
1225
|
200,
|
|
@@ -1167,7 +1231,8 @@ export async function startAgent({
|
|
|
1167
1231
|
}),
|
|
1168
1232
|
);
|
|
1169
1233
|
} catch (error) {
|
|
1170
|
-
json(res, error instanceof DevWorkEventError ? error.status : 502, {
|
|
1234
|
+
json(res, error?.code === "provider_link_disconnected" ? 409 : error instanceof DevWorkEventError ? error.status : 502, {
|
|
1235
|
+
...(error?.code === "provider_link_disconnected" ? { code: error.code } : {}),
|
|
1171
1236
|
error: error instanceof DevWorkEventError ? error.message : "dev-work event failed",
|
|
1172
1237
|
});
|
|
1173
1238
|
}
|
|
@@ -1836,19 +1901,18 @@ export async function startAgent({
|
|
|
1836
1901
|
localTaskRegistry.create(task);
|
|
1837
1902
|
requestClaim.complete(task);
|
|
1838
1903
|
requestClaimCompleted = true;
|
|
1839
|
-
const runLocalTask =
|
|
1840
|
-
|
|
1841
|
-
((localTask) => startLocalTask(localTask, { session, apiOrigin, beforeSpawn: async () => {
|
|
1904
|
+
const runLocalTask = dependencies.runLocalTask || startLocalTask;
|
|
1905
|
+
const runtimeOptions = providerRuntimeOptions(task, { session, apiOrigin, beforeSpawn: async localTask => {
|
|
1842
1906
|
if (purposeWorkspaceBusy(localTask.workspace_path)) throw Object.assign(new Error("Independent verification owns this workspace."), { code: "purpose_verification_workspace_busy" });
|
|
1843
1907
|
if (purposeVerification) {
|
|
1844
1908
|
await authorizeDesktopWork(requestAccessScope);
|
|
1845
1909
|
assertPurposeActor(purposeVerification, session, apiOrigin, localTask, idempotencyKey);
|
|
1846
1910
|
if (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin)) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed" });
|
|
1847
1911
|
}
|
|
1848
|
-
} }));
|
|
1849
|
-
await
|
|
1912
|
+
} }, requestAccessScope, inspectedLinks.get(inspection));
|
|
1913
|
+
await authorizeProviderDispatch(body.backend, requestAccessScope, inspectedLinks.get(inspection));
|
|
1850
1914
|
if (purposeVerification && (purposeStartScope !== desktopScopeRevision() || !isSessionValid(session, Date.now(), apiOrigin))) throw Object.assign(new Error("Purpose verification actor changed."), { code: "purpose_verification_scope_changed", status: 403 });
|
|
1851
|
-
task.operation_promise = Promise.resolve(runLocalTask(task))
|
|
1915
|
+
task.operation_promise = Promise.resolve(runLocalTask(task, runtimeOptions))
|
|
1852
1916
|
.catch(async (error) => {
|
|
1853
1917
|
task.status = "failed";
|
|
1854
1918
|
task.control_state = "complete";
|
|
@@ -1973,13 +2037,20 @@ export async function startAgent({
|
|
|
1973
2037
|
repositoryPath,
|
|
1974
2038
|
});
|
|
1975
2039
|
directRegistry.create(task);
|
|
2040
|
+
const link = inspectedLinks.get(inspection);
|
|
2041
|
+
await authorizeProviderDispatch(body.backend, requestAccessScope, link);
|
|
1976
2042
|
const runDirectTask = dependencies.runDirectTask || startDirectTask;
|
|
2043
|
+
const directOptions = { resolveCommand: async input => {
|
|
2044
|
+
const command = await (dependencies.resolveDesktopExecutorCommand || resolveDesktopExecutorCommand)(input);
|
|
2045
|
+
await authorizeProviderDispatch(body.backend, requestAccessScope, link);
|
|
2046
|
+
return command;
|
|
2047
|
+
} };
|
|
1977
2048
|
const releaseUpdateWork = acquireUpdateWork(updateManager);
|
|
1978
2049
|
if (!releaseUpdateWork) {
|
|
1979
2050
|
json(res, 503, updateInProgressResponse(updateManager));
|
|
1980
2051
|
return;
|
|
1981
2052
|
}
|
|
1982
|
-
void Promise.resolve(runDirectTask(task))
|
|
2053
|
+
void Promise.resolve(runDirectTask(task, directOptions))
|
|
1983
2054
|
.catch((error) => {
|
|
1984
2055
|
task.status = "failed";
|
|
1985
2056
|
task.error = error instanceof Error ? error.message : String(error);
|
|
@@ -2177,7 +2248,8 @@ export async function startAgent({
|
|
|
2177
2248
|
if (session !== launchSession || !isSessionValid(session, Date.now(), apiOrigin)) {
|
|
2178
2249
|
json(res, 409, changedLaunchSession(launchSession, session, apiOrigin)); return;
|
|
2179
2250
|
}
|
|
2180
|
-
|
|
2251
|
+
await bindRunAuthorization(run, requestAccessScope, inspection);
|
|
2252
|
+
void startLocalCodingRun(run, codingRuntimeOptions(run));
|
|
2181
2253
|
json(res, 202, { ok: true, run_id: run.id });
|
|
2182
2254
|
return;
|
|
2183
2255
|
}
|
|
@@ -2321,7 +2393,7 @@ export async function startAgent({
|
|
|
2321
2393
|
const continuation = continueLocalCodingRun(
|
|
2322
2394
|
run,
|
|
2323
2395
|
String(body.message || "Continue after increasing the Provider token budget."),
|
|
2324
|
-
|
|
2396
|
+
codingRuntimeOptions(run),
|
|
2325
2397
|
);
|
|
2326
2398
|
json(res, 202, {
|
|
2327
2399
|
ok: true,
|
|
@@ -2434,7 +2506,7 @@ export async function startAgent({
|
|
|
2434
2506
|
const continuation = continueLocalCodingRunAfterRepositoryApproval(
|
|
2435
2507
|
run,
|
|
2436
2508
|
{ repository: selected.repository, localPath: selected.local_path },
|
|
2437
|
-
|
|
2509
|
+
codingRuntimeOptions(run),
|
|
2438
2510
|
);
|
|
2439
2511
|
json(res, 202, {
|
|
2440
2512
|
ok: true,
|
|
@@ -2479,7 +2551,7 @@ export async function startAgent({
|
|
|
2479
2551
|
|
|
2480
2552
|
json(res, 404, { error: "not found" });
|
|
2481
2553
|
} catch (err) {
|
|
2482
|
-
if (err?.code === "provider_login_required" || err?.code === "provider_status_unknown" ||
|
|
2554
|
+
if (err?.code === "provider_link_disconnected" || err?.code === "provider_login_required" || err?.code === "provider_status_unknown" ||
|
|
2483
2555
|
(typeof err?.code === "string" && err.code.startsWith("desktop_executor_"))) {
|
|
2484
2556
|
json(res, 409, { error: err.message, code: err.code, recovery: err.recovery });
|
|
2485
2557
|
return;
|
|
@@ -2772,7 +2844,8 @@ export async function startAgent({
|
|
|
2772
2844
|
const executionTarget = normalizeDesktopExecutionTarget(input.executionTarget);
|
|
2773
2845
|
if (!requirement) throw new Error("Describe the coding task first.");
|
|
2774
2846
|
if (!repository) throw new Error("Set a repository with /repo owner/name.");
|
|
2775
|
-
|
|
2847
|
+
const inspection = await inspectReadyBackend(backend);
|
|
2848
|
+
if (!inspection.compatible) {
|
|
2776
2849
|
throw new Error(`${backend === "claude" ? "Claude Code" : "Codex"} is not available on PATH.`);
|
|
2777
2850
|
}
|
|
2778
2851
|
const indexedRepository = await ensureRepositoryIndexed(repository);
|
|
@@ -2812,18 +2885,19 @@ export async function startAgent({
|
|
|
2812
2885
|
await saveCodingRunSnapshot(codingRunStateRoot, run.snapshot);
|
|
2813
2886
|
registry.create(run);
|
|
2814
2887
|
await authorizeDesktopWork(scope);
|
|
2815
|
-
|
|
2888
|
+
await bindRunAuthorization(run, scope, inspection);
|
|
2889
|
+
void startLocalCodingRun(run, codingRuntimeOptions(run));
|
|
2816
2890
|
return run;
|
|
2817
2891
|
}),
|
|
2818
2892
|
continue: (run, guidance) =>
|
|
2819
2893
|
withUpdateWork(updateManager, async () => {
|
|
2820
2894
|
await requireRunProvider(run);
|
|
2821
|
-
return continueLocalCodingRun(run, guidance,
|
|
2895
|
+
return continueLocalCodingRun(run, guidance, codingRuntimeOptions(run));
|
|
2822
2896
|
}),
|
|
2823
2897
|
applyDecision: (run, decision) =>
|
|
2824
2898
|
withUpdateWork(updateManager, async () => {
|
|
2825
2899
|
await requireRunProvider(run);
|
|
2826
|
-
return applyLocalHumanDecision(run, decision,
|
|
2900
|
+
return applyLocalHumanDecision(run, decision, codingRuntimeOptions(run));
|
|
2827
2901
|
}),
|
|
2828
2902
|
queueGuidance: (run, guidance) => {
|
|
2829
2903
|
if (enforceDesktopAccess) requireDesktopAccess(desktopAccess.snapshot());
|
package/src/auto-update.js
CHANGED
|
@@ -69,6 +69,10 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
69
69
|
#stopped = false;
|
|
70
70
|
#registryListener = null;
|
|
71
71
|
#activeWork = 0;
|
|
72
|
+
#confirmedUpdate = null;
|
|
73
|
+
#confirmedReady = false;
|
|
74
|
+
#desktopIdle = () => true;
|
|
75
|
+
#applyHolds = 0;
|
|
72
76
|
|
|
73
77
|
constructor(options) {
|
|
74
78
|
super();
|
|
@@ -95,13 +99,47 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
snapshot() {
|
|
98
|
-
return structuredClone(this.#snapshot);
|
|
102
|
+
return structuredClone({ ...this.#snapshot, requires_task_stop: !this.#isIdle() });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Close admission before cancellation begins. The same transaction owns the
|
|
106
|
+
// idle check and installation, so a releasing worker cannot race the stop.
|
|
107
|
+
applyConfirmed(stopWork) {
|
|
108
|
+
if (this.#confirmedUpdate) return this.#confirmedUpdate;
|
|
109
|
+
if (this.#applying) return this.#applying;
|
|
110
|
+
if (!this.#snapshot.target_version || !this.#options.lastRelease) {
|
|
111
|
+
return Promise.reject(new Error('Check for an available update first.'));
|
|
112
|
+
}
|
|
113
|
+
this.#setState('stopping_tasks', { message: 'Stopping unfinished tasks before updating…', error: null });
|
|
114
|
+
this.#confirmedUpdate = (async () => {
|
|
115
|
+
try {
|
|
116
|
+
await Promise.resolve().then(stopWork);
|
|
117
|
+
await this.#checking;
|
|
118
|
+
if (!this.#snapshot.target_version) throw new Error('The update is no longer available.');
|
|
119
|
+
if (!this.#isIdle()) throw new Error('Task shutdown did not finish. Restart SIMY and retry the update.');
|
|
120
|
+
if (!this.#snapshot.automatic || classifyVersionChange(this.#options.currentVersion, this.#snapshot.target_version) === 'major') {
|
|
121
|
+
this.#setState('update_available', { message: 'Tasks stopped. Opening the update installer…' });
|
|
122
|
+
await this.performAction();
|
|
123
|
+
return this.snapshot();
|
|
124
|
+
}
|
|
125
|
+
this.#confirmedReady = true;
|
|
126
|
+
this.#setState('waiting_for_idle', { message: 'Tasks stopped. Installing the update…' });
|
|
127
|
+
return await this.maybeApply();
|
|
128
|
+
} catch (error) {
|
|
129
|
+
this.#setState('failed', { message: 'Update could not finish after stopping tasks. Restart SIMY and retry.',
|
|
130
|
+
error: errorMessage(error), action: updateAction(this.#snapshot.target_version, this.#options) });
|
|
131
|
+
return this.snapshot();
|
|
132
|
+
} finally { this.#confirmedUpdate = null; this.#confirmedReady = false; }
|
|
133
|
+
})();
|
|
134
|
+
return this.#confirmedUpdate;
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
async performAction() {
|
|
102
138
|
const action = this.#snapshot.action;
|
|
103
139
|
if (action?.id === "download_macos_installer" && action.url) {
|
|
104
|
-
|
|
140
|
+
const result = await this.#options.openUrl(action.url);
|
|
141
|
+
if (result?.opened === false) throw new Error("Could not open the update installer download.");
|
|
142
|
+
return result;
|
|
105
143
|
}
|
|
106
144
|
if (action?.command) {
|
|
107
145
|
throw new Error(`Run this command after leaving SIMY: ${action.command}`);
|
|
@@ -109,8 +147,17 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
109
147
|
throw new Error("No SIMY CLI update action is currently available.");
|
|
110
148
|
}
|
|
111
149
|
|
|
150
|
+
// Hold installation across a multi-worker cancellation, even when the last
|
|
151
|
+
// registry event or work lease is released before every worker has drained.
|
|
152
|
+
holdApply() {
|
|
153
|
+
if (["updating", "restarting"].includes(this.#snapshot.state)) throw new Error("SIMY is already updating.");
|
|
154
|
+
this.#applyHolds++;
|
|
155
|
+
let released = false;
|
|
156
|
+
return () => { if (!released) { released = true; this.#applyHolds--; } };
|
|
157
|
+
}
|
|
158
|
+
|
|
112
159
|
acceptsNewWork() {
|
|
113
|
-
return !["updating", "restarting"].includes(this.#snapshot.state);
|
|
160
|
+
return this.#applyHolds === 0 && !this.#confirmedUpdate && !["stopping_tasks", "updating", "restarting"].includes(this.#snapshot.state);
|
|
114
161
|
}
|
|
115
162
|
|
|
116
163
|
beginWork() {
|
|
@@ -127,10 +174,11 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
127
174
|
};
|
|
128
175
|
}
|
|
129
176
|
|
|
130
|
-
start({ registry, restartDaemon } = {}) {
|
|
177
|
+
start({ registry, restartDaemon, isDesktopIdle = () => true } = {}) {
|
|
131
178
|
if (this.#snapshot.state === "disabled" || this.#checkTimer || this.#stopped) return this;
|
|
132
179
|
this.#registry = registry;
|
|
133
180
|
this.#restartDaemon = restartDaemon;
|
|
181
|
+
this.#desktopIdle = isDesktopIdle;
|
|
134
182
|
this.#registryListener = () => {
|
|
135
183
|
if (this.#snapshot.state === "waiting_for_idle") void this.maybeApply();
|
|
136
184
|
};
|
|
@@ -156,7 +204,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
156
204
|
async checkNow() {
|
|
157
205
|
if (this.#stopped || this.#snapshot.state === "disabled") return this.snapshot();
|
|
158
206
|
if (this.#checking) return this.#checking;
|
|
159
|
-
if (["updating", "restarting"].includes(this.#snapshot.state)) return this.snapshot();
|
|
207
|
+
if (["stopping_tasks", "updating", "restarting"].includes(this.#snapshot.state) || this.#confirmedUpdate) return this.snapshot();
|
|
160
208
|
|
|
161
209
|
this.#checking = this.#performCheck().finally(() => {
|
|
162
210
|
this.#checking = null;
|
|
@@ -237,6 +285,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
237
285
|
}
|
|
238
286
|
|
|
239
287
|
async maybeApply() {
|
|
288
|
+
if (this.#confirmedUpdate && !this.#confirmedReady) return this.snapshot();
|
|
240
289
|
if (this.#stopped || this.#snapshot.state !== "waiting_for_idle") {
|
|
241
290
|
return this.snapshot();
|
|
242
291
|
}
|
|
@@ -333,7 +382,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
333
382
|
}
|
|
334
383
|
|
|
335
384
|
#isIdle() {
|
|
336
|
-
return this.#activeWork === 0 && isRegistryIdle(this.#registry);
|
|
385
|
+
return this.#applyHolds === 0 && this.#activeWork === 0 && isRegistryIdle(this.#registry) && this.#desktopIdle();
|
|
337
386
|
}
|
|
338
387
|
|
|
339
388
|
#clearIdleTimer() {
|
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.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.17";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { linkedProvider } from "./delivery-loop.mjs";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
5
6
|
import { homedir } from "node:os";
|
|
@@ -13,12 +14,14 @@ const MAX_DISCOVERY_RECORDS = 16;
|
|
|
13
14
|
|
|
14
15
|
export async function runConnectionReminder({
|
|
15
16
|
env = process.env,
|
|
17
|
+
provider = env.CLAUDE_PLUGIN_ROOT ? "claude" : "codex",
|
|
16
18
|
input = {},
|
|
17
19
|
fetchImpl = globalThis.fetch,
|
|
18
20
|
now = () => Date.now(),
|
|
19
21
|
} = {}) {
|
|
20
22
|
const explicitWebOrigin = resolveExplicitWebOrigin(env);
|
|
21
23
|
const sessionRoot = env.SIMY_HOME?.trim() || join(homedir(), ".simy");
|
|
24
|
+
if (!await linkedProvider(join(sessionRoot, "delivery-hooks"), provider)) return { continue: true };
|
|
22
25
|
const pluginData = env.PLUGIN_DATA?.trim() || env.CLAUDE_PLUGIN_DATA?.trim() || join(sessionRoot, "plugin-data", "simy");
|
|
23
26
|
const sessionId = String(input.session_id || input.sessionId || env.CODEX_THREAD_ID || "unknown");
|
|
24
27
|
const statePath = reminderStatePath(pluginData, explicitWebOrigin || "any-origin", sessionId);
|
|
@@ -172,9 +175,9 @@ async function readHookInput(stream = process.stdin) {
|
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
export async function main() {
|
|
178
|
+
export async function main(provider) {
|
|
176
179
|
try {
|
|
177
|
-
const result = await runConnectionReminder({ input: await readHookInput() });
|
|
180
|
+
const result = await runConnectionReminder({ provider, input: await readHookInput() });
|
|
178
181
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
179
182
|
} catch {
|
|
180
183
|
process.stdout.write(`${JSON.stringify({ continue: true })}\n`);
|