@skrr-ai/cli 0.1.119 → 0.1.120
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/dist/base-command.d.ts +31 -0
- package/dist/base-command.js +106 -4
- package/dist/commands/commands.js +11 -0
- package/dist/commands/commitments/action-proposal.d.ts +13 -0
- package/dist/commands/commitments/action-proposal.js +39 -0
- package/dist/commands/commitments/action-proposals/decide.js +8 -1
- package/dist/commands/commitments/action-proposals/execute.js +11 -1
- package/dist/commands/commitments/create.js +2 -2
- package/dist/commands/commitments/doctor.d.ts +8 -0
- package/dist/commands/commitments/doctor.js +22 -0
- package/dist/commands/commitments/execute-remediation.js +12 -1
- package/dist/commands/commitments/observations.js +2 -2
- package/dist/commands/commitments/pack/bind.d.ts +18 -1
- package/dist/commands/commitments/pack/bind.js +66 -8
- package/dist/commands/commitments/pack/plan.d.ts +4 -1
- package/dist/commands/commitments/pack/plan.js +30 -2
- package/dist/commands/commitments/update.js +8 -2
- package/dist/commands/completion/index.js +1 -1
- package/dist/commands/completion/values.js +1 -1
- package/dist/commands/computer-use/audio.d.ts +1 -0
- package/dist/commands/computer-use/audio.js +1 -0
- package/dist/commands/computer-use/browser.d.ts +1 -0
- package/dist/commands/computer-use/browser.js +1 -0
- package/dist/commands/computer-use/disable.d.ts +1 -0
- package/dist/commands/computer-use/disable.js +1 -0
- package/dist/commands/computer-use/enable.d.ts +1 -0
- package/dist/commands/computer-use/enable.js +1 -0
- package/dist/commands/computer-use/index.d.ts +1 -0
- package/dist/commands/computer-use/index.js +1 -0
- package/dist/commands/computer-use/install.d.ts +1 -0
- package/dist/commands/computer-use/install.js +1 -0
- package/dist/commands/computer-use/recover.d.ts +1 -0
- package/dist/commands/computer-use/recover.js +1 -0
- package/dist/commands/computer-use/status.d.ts +1 -0
- package/dist/commands/computer-use/status.js +1 -0
- package/dist/commands/computer-use/stop.d.ts +1 -0
- package/dist/commands/computer-use/stop.js +1 -0
- package/dist/commands/context.js +16 -11
- package/dist/commands/convos/recovery.js +1 -1
- package/dist/commands/daemon/diagnostics.d.ts +9 -0
- package/dist/commands/daemon/diagnostics.js +53 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/maintenance/attest.d.ts +15 -0
- package/dist/commands/daemon/maintenance/attest.js +52 -0
- package/dist/commands/daemon/maintenance/hold.d.ts +17 -0
- package/dist/commands/daemon/maintenance/hold.js +70 -0
- package/dist/commands/daemon/maintenance/index.d.ts +12 -0
- package/dist/commands/daemon/maintenance/index.js +30 -0
- package/dist/commands/daemon/maintenance/resume.d.ts +13 -0
- package/dist/commands/daemon/maintenance/resume.js +36 -0
- package/dist/commands/daemon/maintenance/status.d.ts +13 -0
- package/dist/commands/daemon/maintenance/status.js +36 -0
- package/dist/commands/doctor.d.ts +4 -1
- package/dist/commands/doctor.js +50 -6
- package/dist/commands/followups/plans/cancel-batch.js +6 -3
- package/dist/commands/followups/plans/capacity.js +7 -3
- package/dist/commands/followups/plans/observability.js +11 -3
- package/dist/commands/followups/plans/preferences/set.js +26 -8
- package/dist/commands/followups/plans/preferences.js +1 -1
- package/dist/commands/followups/plans/show.js +1 -1
- package/dist/commands/goals/key-results/assess.d.ts +36 -0
- package/dist/commands/goals/key-results/assess.js +110 -0
- package/dist/commands/goals/key-results/history.d.ts +30 -0
- package/dist/commands/goals/key-results/history.js +104 -0
- package/dist/commands/goals/key-results/list.d.ts +5 -4
- package/dist/commands/goals/key-results/list.js +12 -6
- package/dist/commands/goals/key-results/show.d.ts +4 -2
- package/dist/commands/goals/key-results/show.js +25 -4
- package/dist/commands/goals/key-results/update.d.ts +4 -0
- package/dist/commands/goals/key-results/update.js +18 -24
- package/dist/commands/goals/plan-now.js +12 -1
- package/dist/commands/goals/planner-compare.d.ts +30 -0
- package/dist/commands/goals/planner-compare.js +136 -0
- package/dist/commands/machines/dedicated/service/exec.d.ts +3 -0
- package/dist/commands/machines/dedicated/service/exec.js +70 -11
- package/dist/commands/machines/hosted/destroy.d.ts +1 -0
- package/dist/commands/machines/hosted/destroy.js +11 -1
- package/dist/commands/machines/hosted/exec.d.ts +1 -0
- package/dist/commands/machines/hosted/exec.js +12 -1
- package/dist/commands/machines/hosted/index.js +1 -0
- package/dist/commands/machines/hosted/operation.d.ts +14 -0
- package/dist/commands/machines/hosted/operation.js +51 -0
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/start.js +6 -2
- package/dist/commands/spaces/auto-executor/compare.d.ts +18 -0
- package/dist/commands/spaces/auto-executor/compare.js +139 -0
- package/dist/commands/spaces/auto-executor/runs.d.ts +3 -0
- package/dist/commands/spaces/auto-executor/runs.js +13 -1
- package/dist/commands/spaces/charter/create-from-description.d.ts +21 -0
- package/dist/commands/spaces/charter/create-from-description.js +77 -0
- package/dist/commands/spaces/charter/show.d.ts +22 -0
- package/dist/commands/spaces/charter/show.js +61 -0
- package/dist/commands/spaces/heartbeats/list.d.ts +1 -16
- package/dist/commands/spaces/heartbeats/list.js +13 -0
- package/dist/commands/spaces/install-template.js +3 -0
- package/dist/commands/spaces/loops/migrate.d.ts +32 -0
- package/dist/commands/spaces/loops/migrate.js +103 -0
- package/dist/commands/spaces/loops/release.d.ts +14 -0
- package/dist/commands/spaces/loops/release.js +59 -0
- package/dist/commands/spaces/notebook.d.ts +25 -0
- package/dist/commands/spaces/notebook.js +75 -0
- package/dist/commands/spaces/proposals/discard.d.ts +17 -0
- package/dist/commands/spaces/proposals/discard.js +17 -0
- package/dist/commands/spaces/proposals/keep.d.ts +17 -0
- package/dist/commands/spaces/proposals/keep.js +17 -0
- package/dist/commands/spaces/proposals/list.d.ts +24 -0
- package/dist/commands/spaces/proposals/list.js +89 -0
- package/dist/commands/spaces/skills/propose.d.ts +29 -0
- package/dist/commands/spaces/skills/propose.js +82 -0
- package/dist/commands/spaces/templates/fork.js +3 -0
- package/dist/commands/spaces/triggers/show.js +6 -0
- package/dist/commands/spaces/workflows/retry.d.ts +1 -0
- package/dist/commands/spaces/workflows/retry.js +8 -1
- package/dist/commands/spaces/workflows/triggers/create.js +3 -2
- package/dist/commands/spaces/workflows/triggers/fire.js +4 -0
- package/dist/commands/spaces/workflows/triggers/update.js +2 -1
- package/dist/commands/spaces/write-policy/set.d.ts +22 -0
- package/dist/commands/spaces/write-policy/set.js +82 -0
- package/dist/commands/spaces/write-policy/show.d.ts +19 -0
- package/dist/commands/spaces/write-policy/show.js +49 -0
- package/dist/commands/store/artifacts/receipt.d.ts +14 -0
- package/dist/commands/store/artifacts/receipt.js +34 -0
- package/dist/commands/store/artifacts/release.d.ts +2 -0
- package/dist/commands/store/artifacts/release.js +11 -0
- package/dist/commands/store/governance/publish-release.d.ts +2 -0
- package/dist/commands/store/governance/publish-release.js +30 -1
- package/dist/commands/store/governance/release-receipt.d.ts +12 -0
- package/dist/commands/store/governance/release-receipt.js +32 -0
- package/dist/commands/tasks/approvals/list.d.ts +11 -5
- package/dist/commands/tasks/approvals/list.js +13 -6
- package/dist/commands/tasks/attachments/facts.d.ts +12 -0
- package/dist/commands/tasks/attachments/facts.js +29 -0
- package/dist/commands/tasks/attachments/upload.d.ts +1 -0
- package/dist/commands/tasks/attachments/upload.js +16 -0
- package/dist/commands/tasks/bulk-update.js +35 -1
- package/dist/commands/tasks/context.d.ts +15 -0
- package/dist/commands/tasks/context.js +69 -0
- package/dist/commands/tasks/copy.d.ts +18 -0
- package/dist/commands/tasks/copy.js +85 -0
- package/dist/commands/tasks/create-tree.js +97 -23
- package/dist/commands/tasks/create.js +9 -1
- package/dist/commands/tasks/deliverable/add.js +20 -10
- package/dist/commands/tasks/move.js +8 -1
- package/dist/commands/tasks/operations/show.d.ts +16 -0
- package/dist/commands/tasks/operations/show.js +98 -0
- package/dist/commands/tasks/operations/wait.d.ts +22 -0
- package/dist/commands/tasks/operations/wait.js +114 -0
- package/dist/commands/tasks/preflight.d.ts +1 -0
- package/dist/commands/tasks/preflight.js +11 -1
- package/dist/commands/tasks/rearm-recurrence.js +4 -0
- package/dist/commands/tasks/result/show.js +22 -0
- package/dist/commands/tasks/result/submit.d.ts +1 -0
- package/dist/commands/tasks/result/submit.js +3 -0
- package/dist/commands/tasks/retry-receipt.d.ts +15 -0
- package/dist/commands/tasks/retry-receipt.js +89 -0
- package/dist/commands/tasks/retry-review.d.ts +16 -0
- package/dist/commands/tasks/retry-review.js +66 -0
- package/dist/commands/tasks/retry.d.ts +4 -8
- package/dist/commands/tasks/retry.js +34 -68
- package/dist/commands/tasks/runs.d.ts +21 -0
- package/dist/commands/tasks/runs.js +95 -0
- package/dist/commands/tasks/show.d.ts +2 -0
- package/dist/commands/tasks/show.js +15 -1
- package/dist/commands/tasks/start-receipt.d.ts +15 -0
- package/dist/commands/tasks/start-receipt.js +95 -0
- package/dist/commands/tasks/start.d.ts +4 -0
- package/dist/commands/tasks/start.js +49 -3
- package/dist/commands/tasks/update.js +3 -0
- package/dist/commands/tasks/upsert.js +17 -3
- package/dist/commands/tasks/watch.d.ts +3 -0
- package/dist/commands/tasks/watch.js +85 -20
- package/dist/commands/wiki/history.d.ts +33 -0
- package/dist/commands/wiki/history.js +134 -0
- package/dist/commands/wiki/ls.d.ts +2 -0
- package/dist/commands/wiki/ls.js +8 -0
- package/dist/commands/wiki/update.d.ts +3 -1
- package/dist/commands/wiki/update.js +24 -2
- package/dist/commands/wiki/write.d.ts +22 -0
- package/dist/commands/wiki/write.js +144 -6
- package/dist/generated/agent-guides.json +81 -81
- package/dist/generated/managed-instruction-guides.json +1 -1
- package/dist/lib/agent-guides.d.ts +9 -0
- package/dist/lib/agent-guides.js +23 -1
- package/dist/lib/agentic-stream.d.ts +5 -0
- package/dist/lib/agentic-stream.js +122 -3
- package/dist/lib/auth-diagnostics.d.ts +39 -0
- package/dist/lib/auth-diagnostics.js +71 -0
- package/dist/lib/cli-client-identity.d.ts +6 -0
- package/dist/lib/cli-client-identity.js +16 -0
- package/dist/lib/cli-installation-diagnostics.d.ts +52 -0
- package/dist/lib/cli-installation-diagnostics.js +254 -0
- package/dist/lib/command-hygiene.d.ts +45 -0
- package/dist/lib/command-hygiene.js +413 -0
- package/dist/lib/commitments.d.ts +64 -3
- package/dist/lib/commitments.js +34 -6
- package/dist/lib/completion-index.d.ts +16 -4
- package/dist/lib/completion-index.js +50 -14
- package/dist/lib/config.js +7 -1
- package/dist/lib/daemon-maintenance-command.d.ts +22 -0
- package/dist/lib/daemon-maintenance-command.js +39 -0
- package/dist/lib/daemon-maintenance.d.ts +275 -0
- package/dist/lib/daemon-maintenance.js +1035 -0
- package/dist/lib/dedicated-service.d.ts +3 -2
- package/dist/lib/dedicated-service.js +11 -3
- package/dist/lib/governed-store.d.ts +8 -0
- package/dist/lib/governed-store.js +5 -0
- package/dist/lib/hosted-machines.d.ts +33 -2
- package/dist/lib/hosted-machines.js +19 -1
- package/dist/lib/json-output.d.ts +1 -1
- package/dist/lib/machines.d.ts +9 -1
- package/dist/lib/machines.js +15 -1
- package/dist/lib/node-adapter.js +13 -0
- package/dist/lib/record-output.d.ts +22 -0
- package/dist/lib/record-output.js +78 -0
- package/dist/lib/roles.d.ts +3 -3
- package/dist/lib/scoped-memory-command.d.ts +18 -0
- package/dist/lib/scoped-memory-command.js +53 -2
- package/dist/lib/space-charter.d.ts +49 -0
- package/dist/lib/space-charter.js +78 -0
- package/dist/lib/space-loop-migration.d.ts +18 -0
- package/dist/lib/space-loop-migration.js +128 -0
- package/dist/lib/space-notebook.d.ts +22 -0
- package/dist/lib/space-notebook.js +140 -0
- package/dist/lib/space-proposal-decision-command.d.ts +36 -0
- package/dist/lib/space-proposal-decision-command.js +108 -0
- package/dist/lib/space-proposals.d.ts +44 -0
- package/dist/lib/space-proposals.js +186 -0
- package/dist/lib/space-templates.d.ts +8 -0
- package/dist/lib/space-templates.js +17 -0
- package/dist/lib/space-write-policy.d.ts +24 -0
- package/dist/lib/space-write-policy.js +103 -0
- package/dist/lib/task-agent-context-capabilities.d.ts +10 -0
- package/dist/lib/task-agent-context-capabilities.js +22 -0
- package/dist/lib/task-agent-context-compatibility.d.ts +9 -0
- package/dist/lib/task-agent-context-compatibility.js +30 -0
- package/dist/lib/task-asset-upload.d.ts +22 -0
- package/dist/lib/task-asset-upload.js +257 -29
- package/dist/lib/task-batch.d.ts +15 -1
- package/dist/lib/task-batch.js +35 -2
- package/dist/lib/task-operation-watch.d.ts +66 -0
- package/dist/lib/task-operation-watch.js +159 -0
- package/dist/lib/task-resolver.d.ts +1 -1
- package/dist/lib/task-resolver.js +4 -3
- package/dist/lib/task-work.d.ts +1 -1
- package/dist/lib/task-work.js +35 -1
- package/dist/lib/task-write-attribution.d.ts +14 -0
- package/dist/lib/task-write-attribution.js +35 -2
- package/dist/lib/triggers.d.ts +11 -0
- package/dist/lib/triggers.js +7 -0
- package/dist/lib/wiki-update.d.ts +1 -0
- package/dist/lib/wiki-update.js +10 -1
- package/dist/lib/wiki.js +4 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +31 -15
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +13 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/guidedUpdate.js +315 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/maintenanceReportWire.js +262 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/toolApprovalSignature.js +106 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +9 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +32 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +33 -14
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.d.ts +370 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/guidedUpdate.js +404 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.d.ts +178 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/maintenanceReportWire.js +342 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.d.ts +51 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/toolApprovalSignature.js +138 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +20 -0
- package/dist/node_modules/@skrr-ai/data-provider/index.js +27085 -20250
- package/oclif.manifest.json +10566 -7247
- package/package.json +20 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure core of a GUIDED update (OSK-19107, Space OSK-S-524, tasks M51 to M53):
|
|
3
|
+
* hold the machine, wait for it to be quiet, update, verify, and give the machine
|
|
4
|
+
* back. The Desktop app and a standalone `skrr` install both run it, so what they
|
|
5
|
+
* decide on, and the words they use for what is in the way, cannot drift apart.
|
|
6
|
+
*
|
|
7
|
+
* This file is DECISIONS ONLY. It performs no I/O, reads no clock and holds no
|
|
8
|
+
* state, so every rule below is a table test. The orchestration (a persisted flow
|
|
9
|
+
* record, polling, the installer, the supervisor) lives with each caller, because
|
|
10
|
+
* that is the part that differs between a Desktop that quits mid-flow and a CLI that
|
|
11
|
+
* runs in the foreground.
|
|
12
|
+
*
|
|
13
|
+
* ## Why these rules (each names the failure it prevents)
|
|
14
|
+
*
|
|
15
|
+
* - **`prepare` is a probe that briefly refuses admitted work.** While it runs the
|
|
16
|
+
* daemon refuses admitted tool calls with an infra-class error, and a tool-proxy Run
|
|
17
|
+
* that sees one drops its tools to the Hosted Machine for good. So a guided flow
|
|
18
|
+
* calls it ONLY when the server's reading says there is nothing admitted to refuse
|
|
19
|
+
* ({@link prepareBasis}), never to find out whether there is.
|
|
20
|
+
* - **`unknown` is an answer.** A source nobody could read is never zero and never
|
|
21
|
+
* ready. {@link noServerVeto} is deliberately strict: every server source must have
|
|
22
|
+
* been read, and none may list active or unproven work.
|
|
23
|
+
* - **A server source can only veto.** Only the daemon's own positively announced
|
|
24
|
+
* report grants `ready`; the owner's attestation and the daemon's own `prepare`
|
|
25
|
+
* (labelled `no_veto`, D38) are the two other bases, and each is labelled as what
|
|
26
|
+
* it is wherever it is shown.
|
|
27
|
+
* - **Blockers are shown in the words of whoever knows them.** The daemon's blocker
|
|
28
|
+
* `reason` and the server's holder `label` are the vocabulary; nothing here
|
|
29
|
+
* re-spells them. They are bounded and never a raw path, because a diagnostic can
|
|
30
|
+
* carry one.
|
|
31
|
+
* - **A new PID, an exit code or a cloud connection is not a verified update.**
|
|
32
|
+
* {@link verifyReplacement} requires the daemon's own `verified`, from a process
|
|
33
|
+
* other than the one that was replaced, for the build that was asked for, and the
|
|
34
|
+
* same machine identity afterwards. A different identity is the reinstall case: the
|
|
35
|
+
* hold is left in place and shown, never resumed.
|
|
36
|
+
*
|
|
37
|
+
* The shapes are STRUCTURAL SUBSETS of `DaemonMaintenanceView` in
|
|
38
|
+
* `@skrr-ai/data-provider` (`daemonMaintenance.ts`), which is the one vocabulary.
|
|
39
|
+
* This package cannot import it (it sits below it), so `guidedUpdate.test.ts` reads
|
|
40
|
+
* that file and fails when a list mirrored here drifts from it.
|
|
41
|
+
*/
|
|
42
|
+
/** The server sources that can only veto. `daemon_report` is the only one that grants. */
|
|
43
|
+
export const GUIDED_SERVER_SOURCES = [
|
|
44
|
+
'claims',
|
|
45
|
+
'receipts',
|
|
46
|
+
'sessions',
|
|
47
|
+
'runs',
|
|
48
|
+
'leases',
|
|
49
|
+
'custody',
|
|
50
|
+
];
|
|
51
|
+
/** The refusals the owner API names, mirrored from `DAEMON_MAINTENANCE_REFUSALS`. */
|
|
52
|
+
export const GUIDED_REFUSALS = [
|
|
53
|
+
'managed_machine',
|
|
54
|
+
'no_stable_identity',
|
|
55
|
+
'not_found',
|
|
56
|
+
'ambiguous',
|
|
57
|
+
'generation_conflict',
|
|
58
|
+
'not_held',
|
|
59
|
+
'already_held',
|
|
60
|
+
'attestation_not_applicable',
|
|
61
|
+
'not_owner',
|
|
62
|
+
'held_by_other',
|
|
63
|
+
];
|
|
64
|
+
export function isGuidedRefusal(value) {
|
|
65
|
+
return typeof value === 'string' && GUIDED_REFUSALS.includes(value);
|
|
66
|
+
}
|
|
67
|
+
/** The daemon handoff phases, mirrored from `daemon/src/update/handoff.ts`. */
|
|
68
|
+
export const GUIDED_HANDOFF_PHASES = [
|
|
69
|
+
'ready',
|
|
70
|
+
'deferred',
|
|
71
|
+
'reconnecting',
|
|
72
|
+
'verified',
|
|
73
|
+
'failed',
|
|
74
|
+
'rolled-back',
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* Whether the server holds a reading that nothing vetoes, for a daemon that cannot
|
|
78
|
+
* report (D38). Strict on purpose: a source that is absent from the list, unreadable,
|
|
79
|
+
* truncated, or that names active or unproven work, is a veto. `daemon_report` must
|
|
80
|
+
* be `not_announced`: any other answer means the reading is not the
|
|
81
|
+
* `no_inventory_capability` case this is for.
|
|
82
|
+
*/
|
|
83
|
+
export function noServerVeto(sources) {
|
|
84
|
+
// `Array.isArray` does not narrow a readonly array, so the list is named explicitly.
|
|
85
|
+
if (!Array.isArray(sources))
|
|
86
|
+
return false;
|
|
87
|
+
const list = sources;
|
|
88
|
+
for (const name of GUIDED_SERVER_SOURCES) {
|
|
89
|
+
const source = list.find((s) => s && s.source === name);
|
|
90
|
+
if (!source || source.read !== 'ok' || source.truncated === true)
|
|
91
|
+
return false;
|
|
92
|
+
const holders = Array.isArray(source.holders)
|
|
93
|
+
? source.holders
|
|
94
|
+
: null;
|
|
95
|
+
if (!holders)
|
|
96
|
+
return false;
|
|
97
|
+
if (holders.some((h) => h && (h.class === 'active' || h.class === 'unproven')))
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const report = list.find((s) => s && s.source === 'daemon_report');
|
|
101
|
+
return report !== undefined && report.read === 'not_announced';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Read the owner API's view for a flow that took `generation`.
|
|
105
|
+
*
|
|
106
|
+
* Order matters and is the rule set (each is a test):
|
|
107
|
+
* 1. No live hold: `not_held`. A different live generation: `generation_changed`.
|
|
108
|
+
* 2. The fence is not confirmed (`pausing`): nothing below it means anything.
|
|
109
|
+
* 3. The reading is `ready` (the daemon's own report): `ready` by evidence.
|
|
110
|
+
* 4. `draining`: work is running. Evidence, never overridden.
|
|
111
|
+
* 5. `unknown`: ready by attestation if the Journey says the owner vouched for this
|
|
112
|
+
* cause; `no_report` if the daemon cannot report and no source vetoes; else
|
|
113
|
+
* `unknown`. No reading at all is `unknown (source_unreadable)`, never ready.
|
|
114
|
+
*/
|
|
115
|
+
export function evaluateReadiness(view, expectedGeneration) {
|
|
116
|
+
const hold = view?.hold;
|
|
117
|
+
if (!view || !hold || hold.lifecycle !== 'held')
|
|
118
|
+
return { kind: 'not_held' };
|
|
119
|
+
if (expectedGeneration !== null && hold.generation !== expectedGeneration) {
|
|
120
|
+
return { kind: 'generation_changed', currentGeneration: hold.generation };
|
|
121
|
+
}
|
|
122
|
+
if (view.journey?.phase === 'pausing')
|
|
123
|
+
return { kind: 'pausing' };
|
|
124
|
+
const reading = view.reading;
|
|
125
|
+
if (!reading)
|
|
126
|
+
return { kind: 'unknown', cause: 'source_unreadable' };
|
|
127
|
+
if (reading.state === 'ready')
|
|
128
|
+
return { kind: 'ready', basis: 'evidence' };
|
|
129
|
+
if (reading.state === 'draining')
|
|
130
|
+
return { kind: 'draining' };
|
|
131
|
+
if (reading.state !== 'unknown')
|
|
132
|
+
return { kind: 'unknown', cause: 'source_unreadable' };
|
|
133
|
+
if (view.journey?.phase === 'ready' && view.journey.modifiers?.includes('attested')) {
|
|
134
|
+
return { kind: 'ready', basis: 'attested' };
|
|
135
|
+
}
|
|
136
|
+
if (reading.cause === 'no_inventory_capability' && noServerVeto(reading.sources)) {
|
|
137
|
+
return { kind: 'no_report' };
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'unknown', cause: reading.cause ?? null };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Whether a guided flow may call the daemon's `prepare`, and on what basis. `null`
|
|
143
|
+
* means NO: `prepare` is never a way to find out. A legacy daemon never gets one:
|
|
144
|
+
* it has no endpoint, and its restart is the owner's attested decision, not a probe.
|
|
145
|
+
*/
|
|
146
|
+
export function prepareBasis(readiness, opts) {
|
|
147
|
+
if (opts.legacy)
|
|
148
|
+
return null;
|
|
149
|
+
if (readiness.kind === 'ready')
|
|
150
|
+
return readiness.basis;
|
|
151
|
+
if (readiness.kind === 'no_report')
|
|
152
|
+
return 'no_veto';
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a legacy daemon (no handoff endpoint) may be restarted by the supervisor.
|
|
157
|
+
* Only on the owner's attestation: the daemon cannot report what it is doing and a
|
|
158
|
+
* SIGTERM ends its wrappers, so nothing but the owner's word can vouch for it.
|
|
159
|
+
*/
|
|
160
|
+
export function legacyRestartAllowed(readiness) {
|
|
161
|
+
return readiness.kind === 'ready' && readiness.basis === 'attested';
|
|
162
|
+
}
|
|
163
|
+
export const MAX_OWNER_BLOCKERS = 20;
|
|
164
|
+
const BLOCKER_TEXT_MAX = 200;
|
|
165
|
+
const ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
|
|
166
|
+
// A diagnostic can carry a local path or a URL. Showing one would leak the layout of
|
|
167
|
+
// someone's machine into a notice, so a reason that looks like one is dropped whole.
|
|
168
|
+
const PATHLIKE = /(?:^|[\s("'=])(?:~|\.{1,2})?[\\/](?:[\w.-]+[\\/])+|[A-Za-z]:[\\/]|\\\\|file:\/\/|\/(?:Users|home|var|tmp|private|opt|etc|usr)\//i;
|
|
169
|
+
/** A sentence safe to put in front of the owner, or `undefined` when it is not. */
|
|
170
|
+
export function sanitizeBlockerText(value, max = BLOCKER_TEXT_MAX) {
|
|
171
|
+
if (typeof value !== 'string')
|
|
172
|
+
return undefined;
|
|
173
|
+
const cleaned = Array.from(value, (ch) => {
|
|
174
|
+
const code = ch.charCodeAt(0);
|
|
175
|
+
return code < 0x20 || code === 0x7f ? ' ' : ch;
|
|
176
|
+
})
|
|
177
|
+
.join('')
|
|
178
|
+
.replace(/\s+/g, ' ')
|
|
179
|
+
.trim();
|
|
180
|
+
if (!cleaned || PATHLIKE.test(cleaned))
|
|
181
|
+
return undefined;
|
|
182
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}…` : cleaned;
|
|
183
|
+
}
|
|
184
|
+
function boundedId(value) {
|
|
185
|
+
return typeof value === 'string' && ID_PATTERN.test(value) ? value : undefined;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Everything in the way, from the three places that know: the server's holders
|
|
189
|
+
* (active and unproven work: `pending_delivery` and `forensic` never block), the
|
|
190
|
+
* daemon's own refusal from `prepare`, and the work the Desktop app itself hosts and
|
|
191
|
+
* ends when it quits (`LocalSessionService` processes and agent browser windows,
|
|
192
|
+
* which the daemon's inventory cannot see).
|
|
193
|
+
*
|
|
194
|
+
* A daemon blocker whose id equals a server holder's ref is the same work seen twice:
|
|
195
|
+
* it is listed once, with the daemon's reason and the server's label. Whether the two
|
|
196
|
+
* ids are in fact comparable was never established (review §6.2 item 2), so a match
|
|
197
|
+
* only MERGES the wording and never hides a blocker that has no match.
|
|
198
|
+
*/
|
|
199
|
+
export function describeBlockers(input) {
|
|
200
|
+
const out = [];
|
|
201
|
+
const serverByRef = new Map();
|
|
202
|
+
const serverHolders = [];
|
|
203
|
+
for (const source of input.view?.reading?.sources ?? []) {
|
|
204
|
+
for (const holder of source?.holders ?? []) {
|
|
205
|
+
if (!holder || (holder.class !== 'active' && holder.class !== 'unproven'))
|
|
206
|
+
continue;
|
|
207
|
+
serverHolders.push(holder);
|
|
208
|
+
if (typeof holder.ref === 'string')
|
|
209
|
+
serverByRef.set(holder.ref, holder);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const merged = new Set();
|
|
213
|
+
for (const blocker of input.daemon ?? []) {
|
|
214
|
+
if (!blocker || typeof blocker.kind !== 'string')
|
|
215
|
+
continue;
|
|
216
|
+
const id = boundedId(blocker.id);
|
|
217
|
+
const holder = blocker.id ? serverByRef.get(blocker.id) : undefined;
|
|
218
|
+
if (holder)
|
|
219
|
+
merged.add(holder);
|
|
220
|
+
const label = holder ? sanitizeBlockerText(holder.label, 120) : undefined;
|
|
221
|
+
out.push({
|
|
222
|
+
source: 'daemon',
|
|
223
|
+
kind: blocker.kind.slice(0, 64),
|
|
224
|
+
text: sanitizeBlockerText(blocker.reason),
|
|
225
|
+
...(label ? { label } : {}),
|
|
226
|
+
...(id ? { id } : {}),
|
|
227
|
+
...(holder?.since !== undefined ? { since: holder.since } : {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
for (const holder of serverHolders) {
|
|
231
|
+
if (merged.has(holder))
|
|
232
|
+
continue;
|
|
233
|
+
const id = boundedId(holder.ref);
|
|
234
|
+
out.push({
|
|
235
|
+
source: 'server',
|
|
236
|
+
kind: String(holder.kind || 'work').slice(0, 64),
|
|
237
|
+
text: sanitizeBlockerText(holder.label, 120),
|
|
238
|
+
...(id ? { id } : {}),
|
|
239
|
+
...(holder.since !== undefined ? { since: holder.since } : {}),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const desktop = input.desktop;
|
|
243
|
+
if (desktop && desktop.localSessions > 0) {
|
|
244
|
+
out.push({ source: 'desktop', kind: 'desktop_local_sessions', count: desktop.localSessions });
|
|
245
|
+
}
|
|
246
|
+
if (desktop && desktop.agentBrowserWindows > 0) {
|
|
247
|
+
out.push({
|
|
248
|
+
source: 'desktop',
|
|
249
|
+
kind: 'desktop_agent_browser',
|
|
250
|
+
count: desktop.agentBrowserWindows,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return out.slice(0, MAX_OWNER_BLOCKERS);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A string that changes whenever the set of things in the way changes. The "update
|
|
257
|
+
* now anyway" confirmation carries the one it was shown, and the action is refused
|
|
258
|
+
* when it no longer matches: a confirmation that names what WAS running is not a
|
|
259
|
+
* confirmation of what is.
|
|
260
|
+
*/
|
|
261
|
+
export function blockersFingerprint(blockers) {
|
|
262
|
+
return blockers
|
|
263
|
+
.map((b) => [b.source, b.kind, b.id ?? '', b.count ?? ''].join('|'))
|
|
264
|
+
.sort()
|
|
265
|
+
.join(';');
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* The labels the owner is shown, as the server records them in the outcome
|
|
269
|
+
* (`acknowledged`): each bounded to the server's 160-character limit.
|
|
270
|
+
*/
|
|
271
|
+
export function acknowledgedLabels(blockers, max = 20) {
|
|
272
|
+
const labels = [];
|
|
273
|
+
for (const b of blockers) {
|
|
274
|
+
const detail = b.label ?? b.text;
|
|
275
|
+
const text = b.count ? `${b.kind} (${b.count})` : detail ? `${b.kind}: ${detail}` : b.kind;
|
|
276
|
+
labels.push(text.slice(0, 160));
|
|
277
|
+
if (labels.length >= max)
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
return labels;
|
|
281
|
+
}
|
|
282
|
+
function isHandoffStatus(value) {
|
|
283
|
+
if (!value || typeof value !== 'object')
|
|
284
|
+
return false;
|
|
285
|
+
const v = value;
|
|
286
|
+
return (v.protocolVersion === 1 &&
|
|
287
|
+
typeof v.handoffId === 'string' &&
|
|
288
|
+
typeof v.phase === 'string' &&
|
|
289
|
+
GUIDED_HANDOFF_PHASES.includes(v.phase) &&
|
|
290
|
+
typeof v.sourceProcessId === 'string' &&
|
|
291
|
+
!!v.target &&
|
|
292
|
+
typeof v.target.sha256 === 'string' &&
|
|
293
|
+
Array.isArray(v.blockers) &&
|
|
294
|
+
Array.isArray(v.expectedSessions));
|
|
295
|
+
}
|
|
296
|
+
export function classifyHandoffReply(input) {
|
|
297
|
+
const { statusCode, body } = input;
|
|
298
|
+
if (statusCode === null)
|
|
299
|
+
return { kind: 'unreachable' };
|
|
300
|
+
if (statusCode === 200) {
|
|
301
|
+
return isHandoffStatus(body) ? { kind: 'ok', status: body } : { kind: 'unreachable' };
|
|
302
|
+
}
|
|
303
|
+
if (statusCode === 404)
|
|
304
|
+
return { kind: 'absent' };
|
|
305
|
+
if (statusCode === 401 || statusCode === 403)
|
|
306
|
+
return { kind: 'unauthorized' };
|
|
307
|
+
if (statusCode === 400 || statusCode === 409 || statusCode === 413 || statusCode === 405) {
|
|
308
|
+
const code = body && typeof body === 'object' && typeof body.error === 'string'
|
|
309
|
+
? String(body.error).slice(0, 64)
|
|
310
|
+
: undefined;
|
|
311
|
+
return { kind: 'refused', ...(code ? { code } : {}) };
|
|
312
|
+
}
|
|
313
|
+
return { kind: 'unreachable' };
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Decide whether a committed replacement is complete. All of (1) and (2) must hold
|
|
317
|
+
* before anything else is asked of the server (3, {@link serverSawNewProcess}), and
|
|
318
|
+
* nothing here resumes: the verdict only says what was seen.
|
|
319
|
+
*
|
|
320
|
+
* An identity that is merely UNREADABLE (the bootstrap is mid-rotation, the machine
|
|
321
|
+
* payload could not be fetched) is `not_yet`, never a reinstall and never verified.
|
|
322
|
+
*/
|
|
323
|
+
export function verifyReplacement(evidence) {
|
|
324
|
+
const { handoff, expect, identity } = evidence;
|
|
325
|
+
if (!handoff)
|
|
326
|
+
return { kind: 'not_yet', missing: 'daemon_status' };
|
|
327
|
+
if (handoff.phase === 'failed' || handoff.phase === 'rolled-back') {
|
|
328
|
+
return {
|
|
329
|
+
kind: 'failed',
|
|
330
|
+
phase: handoff.phase,
|
|
331
|
+
reason: String(handoff.reason || handoff.phase).slice(0, 64),
|
|
332
|
+
// A failed handoff observed by the SAME process is a replacement that never
|
|
333
|
+
// happened: the old daemon is still the one running, and a retry is meaningful.
|
|
334
|
+
sameProcess: handoff.observer?.processId === expect.sourceProcessId,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (handoff.phase !== 'verified')
|
|
338
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
339
|
+
const by = handoff.verifiedBy;
|
|
340
|
+
if (!by ||
|
|
341
|
+
by.processId === expect.sourceProcessId ||
|
|
342
|
+
handoff.target.sha256 !== expect.targetSha256 ||
|
|
343
|
+
(expect.version !== null && by.version !== expect.version)) {
|
|
344
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
345
|
+
}
|
|
346
|
+
if (identity.bootstrapDaemonId === null || identity.runtimeKeyNow === null) {
|
|
347
|
+
return { kind: 'not_yet', missing: 'identity_unreadable' };
|
|
348
|
+
}
|
|
349
|
+
if (identity.bootstrapDaemonId !== identity.recordedDaemonId) {
|
|
350
|
+
return { kind: 'reinstall', what: 'daemon_id' };
|
|
351
|
+
}
|
|
352
|
+
if (identity.runtimeKeyNow !== identity.recordedRuntimeKey) {
|
|
353
|
+
return { kind: 'reinstall', what: 'runtime_key' };
|
|
354
|
+
}
|
|
355
|
+
return { kind: 'verified' };
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The legacy counterpart: a daemon with no handoff cannot say `verified`, so the copy
|
|
359
|
+
* claims only what was seen ("restarted on version X"), never "adopted N sessions".
|
|
360
|
+
* A new process (a different start time and, where known, a different PID), the same
|
|
361
|
+
* identity, and the version it announces equal to the bundled one.
|
|
362
|
+
*/
|
|
363
|
+
export function verifyLegacyRestart(evidence) {
|
|
364
|
+
const { before, after, expect } = evidence;
|
|
365
|
+
if (after.daemonId === null || after.startedAt === null || evidence.runtimeKeyNow === null) {
|
|
366
|
+
return { kind: 'not_yet', missing: 'identity_unreadable' };
|
|
367
|
+
}
|
|
368
|
+
const newProcess = before.startedAt === null
|
|
369
|
+
? after.processId !== null && after.processId !== before.processId
|
|
370
|
+
: after.startedAt > before.startedAt &&
|
|
371
|
+
(after.processId === null ||
|
|
372
|
+
before.processId === null ||
|
|
373
|
+
after.processId !== before.processId);
|
|
374
|
+
if (!newProcess)
|
|
375
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
376
|
+
if (after.daemonId !== expect.daemonId)
|
|
377
|
+
return { kind: 'reinstall', what: 'daemon_id' };
|
|
378
|
+
if (evidence.runtimeKeyNow !== evidence.recordedRuntimeKey) {
|
|
379
|
+
return { kind: 'reinstall', what: 'runtime_key' };
|
|
380
|
+
}
|
|
381
|
+
if (expect.version !== null && after.version !== expect.version) {
|
|
382
|
+
return { kind: 'not_yet', missing: 'verified' };
|
|
383
|
+
}
|
|
384
|
+
return { kind: 'verified' };
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Whether the server independently saw a new process register under the held key
|
|
388
|
+
* (condition 3). The Journey says `back_held` for ANY process that registered since
|
|
389
|
+
* the fence, so it can only corroborate THIS replacement when it did not already say
|
|
390
|
+
* so before the commit. When it did, the answer is `not_independent`: the flow rests
|
|
391
|
+
* on (1) and (2) and says so.
|
|
392
|
+
*/
|
|
393
|
+
export function serverSawNewProcess(input) {
|
|
394
|
+
if (input.backHeldAtCommit)
|
|
395
|
+
return 'not_independent';
|
|
396
|
+
return input.journeyPhase === 'back_held' ? 'seen' : 'not_yet';
|
|
397
|
+
}
|
|
398
|
+
/** Whether an observed hold is the one this flow took, and still live. */
|
|
399
|
+
export function isOurLiveHold(view, generation) {
|
|
400
|
+
return (generation !== null &&
|
|
401
|
+
!!view?.hold &&
|
|
402
|
+
view.hold.lifecycle === 'held' &&
|
|
403
|
+
view.hold.generation === generation);
|
|
404
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The maintenance report wire contract (Machine Maintenance, OSK-19107, task M24;
|
|
3
|
+
* `docs/architecture/machine-maintenance/implementation-plan-2026-10-24.md` §3.5,
|
|
4
|
+
* `phase0/m04-quiescence-evidence.md` §2.5 and §2.6).
|
|
5
|
+
*
|
|
6
|
+
* Declared ONCE, here. The daemon is published and cannot import the API, so the
|
|
7
|
+
* capability name, the payload shape and its bounds live in this package, which
|
|
8
|
+
* every end can import (`@skrr-ai/auth-core/maintenance-report-wire`).
|
|
9
|
+
* `maintenanceReportWire.test.ts` fails on a second spelling of the capability
|
|
10
|
+
* anywhere else in the repo. The shape follows `dedicatedRuntimeBusyWire.ts`, the
|
|
11
|
+
* precedent for a positively announced, fail-closed report, and differs from it
|
|
12
|
+
* only where the owner's pause needs more.
|
|
13
|
+
*
|
|
14
|
+
* WHAT THE REPORT SAYS
|
|
15
|
+
* ---------------------------------------------------------------------------
|
|
16
|
+
* "What does this daemon process hold right now that an update would take from
|
|
17
|
+
* the owner, and has anything been admitted since the last time you asked?" It is
|
|
18
|
+
* a LOCAL read summarised into counts, the handoff's own restart blockers, and
|
|
19
|
+
* the daemon's own update handoff phase.
|
|
20
|
+
*
|
|
21
|
+
* It is EVIDENCE, never authority. The fence does not depend on it: a daemon that
|
|
22
|
+
* lies, or one that sends nothing, cannot make a held machine accept work. Only
|
|
23
|
+
* this report can grant `ready` (a server-side source can only veto it), and a
|
|
24
|
+
* daemon that never announces the capability can only reach `ready` by the
|
|
25
|
+
* owner's attestation.
|
|
26
|
+
*
|
|
27
|
+
* FAIL CLOSED, in three places
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* 1. A count is `0` only when the daemon positively counted. A source that could
|
|
30
|
+
* not be read is `complete: 'unknown'`; a daemon that is leaving is
|
|
31
|
+
* `complete: false`. A reader MUST treat both as busy, whatever the counts
|
|
32
|
+
* say (`maintenanceReportIsQuiet` is the one place that says so).
|
|
33
|
+
* 2. A report that is malformed in ANY field is `null` from
|
|
34
|
+
* `parseMaintenanceReport`, and the caller treats the machine as not quiet.
|
|
35
|
+
* A count that is missing, negative, fractional or not a safe integer is not
|
|
36
|
+
* "zero": it is a different, broken report. A version this parser does not
|
|
37
|
+
* know is `null` too: a newer shape needs its own announced capability.
|
|
38
|
+
* 3. Bounds. The blocker list and every string are clamped, so one report can
|
|
39
|
+
* never be large enough to matter, and a field that is NOT in the contract is
|
|
40
|
+
* never copied into what the parser returns (the result is built from a
|
|
41
|
+
* fresh object, so `__proto__` and its kin cannot ride along).
|
|
42
|
+
*
|
|
43
|
+
* `admittedTotal`
|
|
44
|
+
* ---------------------------------------------------------------------------
|
|
45
|
+
* A MONOTONIC counter of admissions since this process started. The server proves
|
|
46
|
+
* "nothing was admitted between two reports" by comparing two values, without
|
|
47
|
+
* trusting the daemon's clock. It counts at every place the daemon admits work
|
|
48
|
+
* and OVER-counts on purpose (one admission may be noted at the gate and again
|
|
49
|
+
* where the work registers): a second increment can only make the proof harder,
|
|
50
|
+
* never easier. A path that the daemon cannot count must say so in
|
|
51
|
+
* `daemon/src/maintenance-report.ts`, not hide in a zero.
|
|
52
|
+
*/
|
|
53
|
+
export declare const MAINTENANCE_REPORT_CAPABILITY = "maintenance_report_v1";
|
|
54
|
+
/** The heartbeat (and register) key the report rides under. Sent only by a daemon that announced the capability. */
|
|
55
|
+
export declare const MAINTENANCE_REPORT_HEARTBEAT_KEY = "maintenanceReport";
|
|
56
|
+
export declare const MAINTENANCE_REPORT_VERSION = 1;
|
|
57
|
+
/**
|
|
58
|
+
* The daemon's own update handoff phases (`daemon/src/update/handoff.ts`), and the
|
|
59
|
+
* members of `DaemonMaintenanceHandoff.phase` in `@skrr-ai/data-provider`. A phase
|
|
60
|
+
* outside this list is a newer protocol and makes the whole report `null`.
|
|
61
|
+
*/
|
|
62
|
+
export declare const MAINTENANCE_REPORT_HANDOFF_PHASES: readonly ["ready", "deferred", "reconnecting", "verified", "failed", "rolled-back"];
|
|
63
|
+
export type MaintenanceReportHandoffPhase = (typeof MAINTENANCE_REPORT_HANDOFF_PHASES)[number];
|
|
64
|
+
export declare const MAINTENANCE_REPORT_LIMITS: {
|
|
65
|
+
/** The handoff's blockers carried for the "waiting on" text; the counts, not this list, decide busy. */
|
|
66
|
+
readonly maxBlockers: 32;
|
|
67
|
+
readonly maxKindLength: 32;
|
|
68
|
+
readonly maxIdLength: 128;
|
|
69
|
+
readonly maxReasonLength: 240;
|
|
70
|
+
readonly maxHandoffIdLength: 64;
|
|
71
|
+
readonly maxHandoffReasonLength: 64;
|
|
72
|
+
readonly maxProcessStartIdLength: 64;
|
|
73
|
+
/** Hard ceiling on the serialized report. The clamps above keep a valid one well under it. */
|
|
74
|
+
readonly maxSerializedBytes: 20000;
|
|
75
|
+
};
|
|
76
|
+
/** What `complete` can be. Anything but `true` means the counts prove nothing. */
|
|
77
|
+
export type MaintenanceReportComplete = boolean | 'unknown';
|
|
78
|
+
/** The handoff's own `RestartBlocker` shape (`daemon/src/update/handoff.ts`). Reason text is the handoff's, never re-spelled. */
|
|
79
|
+
export interface MaintenanceReportBlocker {
|
|
80
|
+
kind: string;
|
|
81
|
+
id?: string;
|
|
82
|
+
reason: string;
|
|
83
|
+
}
|
|
84
|
+
export interface MaintenanceReportInflight {
|
|
85
|
+
/** Provider sessions and their wrapper processes: in-process turns, preparing dispatches, live wrappers. */
|
|
86
|
+
sessions: number;
|
|
87
|
+
/** Tool calls running, plus background commands still running. */
|
|
88
|
+
tools: number;
|
|
89
|
+
/** Terminals the daemon holds, preservable or not. A person's open shell is work an update would interrupt. */
|
|
90
|
+
terminals: number;
|
|
91
|
+
/** Provider turns that still depend on credentials, permissions, a browser or a model connection the service owns. */
|
|
92
|
+
providerDependencies: number;
|
|
93
|
+
/** Write-ahead entries whose durability the daemon has not confirmed. */
|
|
94
|
+
walUnconfirmed: number;
|
|
95
|
+
}
|
|
96
|
+
export interface MaintenanceReportHandoff {
|
|
97
|
+
phase: MaintenanceReportHandoffPhase;
|
|
98
|
+
handoffId: string;
|
|
99
|
+
reason?: string;
|
|
100
|
+
/** ISO time the daemon's lease on this phase ends. */
|
|
101
|
+
expiresAt?: string;
|
|
102
|
+
}
|
|
103
|
+
export interface MaintenanceReport {
|
|
104
|
+
version: typeof MAINTENANCE_REPORT_VERSION;
|
|
105
|
+
/** The daemon process that read this. The server drops a report that is not its registered process's. */
|
|
106
|
+
processStartId: string;
|
|
107
|
+
/** ISO time of the daemon's own read. Informational: freshness is the server's receipt time. */
|
|
108
|
+
readAt: string;
|
|
109
|
+
/** `true`: every source was read. `'unknown'`: one could not be. `false`: the process is leaving. */
|
|
110
|
+
complete: MaintenanceReportComplete;
|
|
111
|
+
/** Monotonic count of admissions since this process started. */
|
|
112
|
+
admittedTotal: number;
|
|
113
|
+
inflight: MaintenanceReportInflight;
|
|
114
|
+
blockers: MaintenanceReportBlocker[];
|
|
115
|
+
/** True when the daemon had more blockers than it sent. */
|
|
116
|
+
blockersTruncated: boolean;
|
|
117
|
+
/** The daemon's own update handoff, only while it is live or unresolved; otherwise `null`. */
|
|
118
|
+
handoff: MaintenanceReportHandoff | null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The reader's view of a daemon's heartbeat section. Anything malformed, from a
|
|
122
|
+
* daemon that did not announce the capability, an unknown version, or oversized
|
|
123
|
+
* is `null`, and the caller must then treat the machine as NOT quiet. Never
|
|
124
|
+
* trusts a field it did not validate and never copies one it does not know.
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseMaintenanceReport(raw: unknown): MaintenanceReport | null;
|
|
127
|
+
/**
|
|
128
|
+
* A report as it was stored (JSON text on the presence row). Anything that does
|
|
129
|
+
* not parse, or does not validate, is `null`: a stored value is an input like any
|
|
130
|
+
* other and is read with the same suspicion.
|
|
131
|
+
*/
|
|
132
|
+
export declare function parseStoredMaintenanceReport(stored: unknown): MaintenanceReport | null;
|
|
133
|
+
/**
|
|
134
|
+
* The one definition of a POSITIVE ZERO: the daemon read every source and found
|
|
135
|
+
* nothing. `complete: 'unknown'` and `complete: false` are never zero whatever
|
|
136
|
+
* the counts say, because the counts were not (all) read.
|
|
137
|
+
*
|
|
138
|
+
* A blocker that is not work (a missing restart owner, say) does not make a
|
|
139
|
+
* report non-quiet; readiness is "nothing is running", not "the daemon can
|
|
140
|
+
* restart itself".
|
|
141
|
+
*/
|
|
142
|
+
export declare function maintenanceReportIsQuiet(report: MaintenanceReport | null): boolean;
|
|
143
|
+
export interface BuildMaintenanceReportInput {
|
|
144
|
+
processStartId: string;
|
|
145
|
+
/** Epoch ms of the daemon's own read. */
|
|
146
|
+
readAtMs: number;
|
|
147
|
+
complete: MaintenanceReportComplete;
|
|
148
|
+
admittedTotal: number;
|
|
149
|
+
inflight: MaintenanceReportInflight;
|
|
150
|
+
blockers: ReadonlyArray<{
|
|
151
|
+
kind: string;
|
|
152
|
+
id?: string;
|
|
153
|
+
reason: string;
|
|
154
|
+
}>;
|
|
155
|
+
/** `expiresAtMs` is epoch ms; the wire carries ISO. */
|
|
156
|
+
handoff: {
|
|
157
|
+
phase: MaintenanceReportHandoffPhase;
|
|
158
|
+
handoffId: string;
|
|
159
|
+
reason?: string;
|
|
160
|
+
expiresAtMs?: number;
|
|
161
|
+
} | null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A report that says nothing was counted. The shape the daemon sends when it
|
|
165
|
+
* could not read an inventory source at all, and the shape `buildMaintenanceReport`
|
|
166
|
+
* falls back to when it was handed something the contract cannot carry.
|
|
167
|
+
*/
|
|
168
|
+
export declare function unknownMaintenanceReport(processStartId: string, readAtMs: number, admittedTotal?: number): MaintenanceReport;
|
|
169
|
+
/**
|
|
170
|
+
* Build the wire report from what the daemon read. Input the contract cannot
|
|
171
|
+
* carry (a negative count, a blocker with no kind, a time that is not a time)
|
|
172
|
+
* becomes an `unknown` report rather than a throw or a report that parses to
|
|
173
|
+
* something else, so the result parses whenever `processStartId` is a real id (the
|
|
174
|
+
* daemon's always is). A builder that threw would turn a bad read into silence,
|
|
175
|
+
* and silence is the absence of a report, which the server reads the same way,
|
|
176
|
+
* but later.
|
|
177
|
+
*/
|
|
178
|
+
export declare function buildMaintenanceReport(input: BuildMaintenanceReportInput): MaintenanceReport;
|