@ouro.bot/cli 0.1.0-alpha.732 → 0.1.0-alpha.734
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 +1 -1
- package/changelog.json +34 -0
- package/deploy/unraid/Dockerfile +27 -0
- package/deploy/unraid/README.txt +1368 -0
- package/deploy/unraid/audit-container-spec.sh +4 -0
- package/deploy/unraid/container-runtime.json +1 -0
- package/deploy/unraid/sanctuary-acceptance-adapter.sh +37 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +200 -0
- package/deploy/unraid/sanctuary-acceptance-harness.sh +23 -0
- package/deploy/unraid/sanctuary-deployment-target.mjs +705 -0
- package/deploy/unraid/sanctuary-safe-rename.py +24 -0
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +1777 -0
- package/deploy/unraid/sanctuary-unit16-run.sh +502 -0
- package/deploy/unraid/sanctuary-unit18-target-audit.sh +27 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +24 -0
- package/deploy/unraid/sanctuary.ouro/arc/README.md +3 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +5 -0
- package/deploy/unraid/sanctuary.ouro/habits/sanctuary-health.md +8 -0
- package/deploy/unraid/sanctuary.ouro/provider-readiness.json +18 -0
- package/deploy/unraid/sanctuary.ouro/psyche/ASPIRATIONS.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/LORE.md +3 -0
- package/deploy/unraid/sanctuary.ouro/psyche/SOUL.md +5 -0
- package/deploy/unraid/sanctuary.ouro/psyche/TACIT.md +7 -0
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +21 -0
- package/deploy/unraid/sanctuary.xml +25 -0
- package/dist/heart/approval-files.js +113 -0
- package/dist/heart/approval-store.js +168 -9
- package/dist/heart/auth/auth-flow.js +17 -1
- package/dist/heart/autonomy-budget.js +44 -1
- package/dist/heart/awaiting/await-scheduler.js +6 -2
- package/dist/heart/config.js +8 -0
- package/dist/heart/core.js +117 -132
- package/dist/heart/daemon/cli-exec.js +74 -2
- package/dist/heart/daemon/cli-help.js +6 -1
- package/dist/heart/daemon/cli-parse.js +6 -4
- package/dist/heart/daemon/container-credential-bootstrap.js +185 -0
- package/dist/heart/daemon/container-healthcheck.js +94 -0
- package/dist/heart/daemon/container-runtime.js +84 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +156 -0
- package/dist/heart/daemon/container-spec-auditor.js +156 -0
- package/dist/heart/daemon/daemon-bootstrap-startup.js +41 -0
- package/dist/heart/daemon/daemon-entry.js +116 -28
- package/dist/heart/daemon/daemon-health.js +10 -0
- package/dist/heart/daemon/daemon.js +92 -5
- package/dist/heart/daemon/doctor.js +56 -7
- package/dist/heart/daemon/runtime-logging.js +6 -0
- package/dist/heart/daemon/runtime-mode.js +13 -2
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +2365 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1624 -0
- package/dist/heart/daemon/sanctuary-acceptance-marker.js +267 -0
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +985 -0
- package/dist/heart/daemon/sanctuary-scheduler-liveness.js +227 -0
- package/dist/heart/daemon/sanctuary-scheduler-origin.js +126 -0
- package/dist/heart/daemon/sense-manager.js +25 -6
- package/dist/heart/daemon/supercronic-supervisor.js +267 -0
- package/dist/heart/habits/habit-scheduler.js +7 -3
- package/dist/heart/identity.js +13 -3
- package/dist/heart/provider-credentials.js +9 -1
- package/dist/heart/provider-models.js +8 -0
- package/dist/heart/provider-ping.js +5 -2
- package/dist/heart/providers/openai-compatible.js +177 -0
- package/dist/heart/runtime-credentials.js +217 -8
- package/dist/heart/sense-truth.js +2 -0
- package/dist/heart/tool-approval.js +1 -0
- package/dist/heart/turn-context.js +8 -0
- package/dist/mailroom/blob-store.js +97 -19
- package/dist/mailroom/cache-sync-cli.js +5 -1
- package/dist/mailroom/core.js +23 -8
- package/dist/mailroom/hosted-cache-sync.js +179 -44
- package/dist/mailroom/search-cache.js +109 -0
- package/dist/mind/prompt.js +9 -0
- package/dist/nerves/coverage/source-scanner.js +4 -4
- package/dist/nerves/index.js +75 -9
- package/dist/nerves/runtime.js +12 -0
- package/dist/repertoire/tool-arguments.js +39 -0
- package/dist/repertoire/tools-unraid.js +353 -0
- package/dist/repertoire/tools.js +19 -1
- package/dist/repertoire/unraid-client.js +155 -0
- package/dist/repertoire/unraid-restart.js +130 -0
- package/dist/senses/private-runtime-worker.js +3 -2
- package/dist/senses/sanctuary-grounding.js +118 -0
- package/dist/senses/sanctuary-health-acceptance-probe-entry.js +12 -0
- package/dist/senses/sanctuary-health-acceptance-probe.js +772 -0
- package/dist/senses/sanctuary-health-runner.js +146 -0
- package/dist/senses/sanctuary-health.js +447 -0
- package/dist/senses/sanctuary-interactive-control.js +306 -0
- package/dist/senses/sanctuary-runtime.js +206 -0
- package/dist/senses/shared-turn.js +11 -3
- package/dist/senses/telegram-approval-runtime.js +526 -0
- package/dist/senses/telegram-audit-ledger.js +220 -0
- package/dist/senses/telegram-client.js +1471 -0
- package/dist/senses/telegram-entry.js +80 -0
- package/dist/senses/telegram.js +1232 -0
- package/npm-shrinkwrap.json +6558 -0
- package/package.json +4 -2
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.proveSanctuaryAttemptedRecoveryWithoutRetry = proveSanctuaryAttemptedRecoveryWithoutRetry;
|
|
37
|
+
exports.executeSanctuaryInteractiveEngine = executeSanctuaryInteractiveEngine;
|
|
38
|
+
exports.createSanctuaryInteractiveControl = createSanctuaryInteractiveControl;
|
|
39
|
+
exports.sanctuaryInteractiveControlReady = sanctuaryInteractiveControlReady;
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
const node_fs_1 = require("node:fs");
|
|
42
|
+
const node_net_1 = require("node:net");
|
|
43
|
+
const path = __importStar(require("node:path"));
|
|
44
|
+
const approval_store_1 = require("../heart/approval-store");
|
|
45
|
+
const tool_approval_1 = require("../heart/tool-approval");
|
|
46
|
+
const runtime_1 = require("../nerves/runtime");
|
|
47
|
+
const telegram_client_1 = require("./telegram-client");
|
|
48
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
49
|
+
const MAX_CONTROL_REQUEST = 16 * 1024;
|
|
50
|
+
function object(value, label) {
|
|
51
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
52
|
+
throw new Error(`${label} must be an object`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function exactKeys(value, keys, label) {
|
|
56
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...keys].sort()))
|
|
57
|
+
throw new Error(`${label} shape is invalid`);
|
|
58
|
+
}
|
|
59
|
+
function sha256(value) {
|
|
60
|
+
return (0, node_crypto_1.createHash)("sha256").update(value, "utf8").digest("hex");
|
|
61
|
+
}
|
|
62
|
+
function exactApproval(projections) {
|
|
63
|
+
const matches = projections.filter(({ approval }) => approval.toolName === "unraid_restart_container" && approval.arguments.container === "calibre-web");
|
|
64
|
+
if (matches.length !== 1)
|
|
65
|
+
throw new Error("interactive production approval is absent or ambiguous");
|
|
66
|
+
return matches[0];
|
|
67
|
+
}
|
|
68
|
+
function exactPending(records, approvalId) {
|
|
69
|
+
const validCallback = (value) => typeof value === "string" && Buffer.byteLength(value) >= 1 && Buffer.byteLength(value) <= 64;
|
|
70
|
+
const matches = records.filter((record) => record.approvalId === approvalId && record.deliveryState === "bound" && record.messageId !== null
|
|
71
|
+
&& validCallback(record.approveCallbackData) && validCallback(record.denyCallbackData));
|
|
72
|
+
if (matches.length !== 1)
|
|
73
|
+
throw new Error("interactive production pending approval is absent or ambiguous");
|
|
74
|
+
return matches[0];
|
|
75
|
+
}
|
|
76
|
+
function exactTerminalTombstone(records, approvalId) {
|
|
77
|
+
const validCallback = (value) => typeof value === "string" && Buffer.byteLength(value) >= 1 && Buffer.byteLength(value) <= 64;
|
|
78
|
+
const matches = records.filter((record) => record.approvalId === approvalId && record.deliveryState === "terminal_tombstone" && record.messageId !== null
|
|
79
|
+
&& validCallback(record.approveCallbackData) && validCallback(record.denyCallbackData)
|
|
80
|
+
&& record.expiryObservation?.schemaVersion === "telegram-approval-expiry-observation-v1"
|
|
81
|
+
&& record.expiryObservation.deadlineAt === record.expiresAt && Number.isSafeInteger(record.expiryObservation.observedAt)
|
|
82
|
+
&& record.expiryObservation.observedAt >= record.expiresAt && typeof record.expiryObservation.evidenceMac === "string" && SHA256.test(record.expiryObservation.evidenceMac)
|
|
83
|
+
&& Number.isSafeInteger(record.terminalizedAt) && Number(record.terminalizedAt) >= record.expiryObservation.observedAt);
|
|
84
|
+
if (matches.length !== 1)
|
|
85
|
+
throw new Error("interactive production terminal tombstone is absent or ambiguous");
|
|
86
|
+
return matches[0];
|
|
87
|
+
}
|
|
88
|
+
function pendingDigest(record) {
|
|
89
|
+
return sha256(JSON.stringify({ approvalId: record.approvalId, messageId: record.messageId, deliveryState: record.deliveryState, approveCallbackData: record.approveCallbackData, denyCallbackData: record.denyCallbackData, expiresAt: record.expiresAt }));
|
|
90
|
+
}
|
|
91
|
+
function proveSanctuaryAttemptedRecoveryWithoutRetry(agentRoot, scenarioHandleDigest, approval) {
|
|
92
|
+
if (!SHA256.test(scenarioHandleDigest))
|
|
93
|
+
throw new Error("attempted recovery scenario is invalid");
|
|
94
|
+
const databaseRoot = path.join(agentRoot, "state", "acceptance", "attempted-recovery-probes");
|
|
95
|
+
(0, node_fs_1.mkdirSync)(databaseRoot, { recursive: true, mode: 0o700 });
|
|
96
|
+
const databasePath = path.join(databaseRoot, `${scenarioHandleDigest}.sqlite`);
|
|
97
|
+
if ((0, node_fs_1.existsSync)(databasePath))
|
|
98
|
+
throw new Error("attempted recovery proof requires inspect-before-retry");
|
|
99
|
+
const instant = new Date("2026-08-21T00:00:00.000Z");
|
|
100
|
+
const first = (0, approval_store_1.openApprovalStore)({ databasePath, now: () => instant });
|
|
101
|
+
const ownerId = "acceptance-attempted-recovery";
|
|
102
|
+
const toolCallId = `acceptance-${approval.toolCallId}`;
|
|
103
|
+
const frozenAssistantMessage = structuredClone(approval.frozenAssistantMessage);
|
|
104
|
+
const toolCalls = frozenAssistantMessage.tool_calls;
|
|
105
|
+
if (!Array.isArray(toolCalls) || toolCalls.length !== 1 || !toolCalls[0] || typeof toolCalls[0] !== "object" || Array.isArray(toolCalls[0])) {
|
|
106
|
+
first.close();
|
|
107
|
+
throw new Error("attempted recovery frozen tool call is invalid");
|
|
108
|
+
}
|
|
109
|
+
toolCalls[0].id = toolCallId;
|
|
110
|
+
const prepared = first.prepare({
|
|
111
|
+
toolCallId, toolName: approval.toolName, arguments: approval.arguments,
|
|
112
|
+
schemaDigest: approval.schemaDigest, toolDigest: approval.toolDigest, policyDigest: approval.policyDigest, policyId: approval.policyId,
|
|
113
|
+
sessionKey: "acceptance:attempted-recovery", sessionPath: path.join(databaseRoot, `${scenarioHandleDigest}.session.json`),
|
|
114
|
+
baseSessionRevision: approval.baseSessionRevision, checkpointDigest: approval.checkpointDigest,
|
|
115
|
+
requesterId: "acceptance-probe", transport: "telegram", transportUserId: "acceptance-probe", transportChatId: "acceptance-probe",
|
|
116
|
+
expiresAt: "2099-01-01T00:00:00.000Z", frozenAssistantMessage,
|
|
117
|
+
});
|
|
118
|
+
first.activate({ approvalId: prepared.record.approvalId, checkpointDigest: prepared.record.checkpointDigest, suspendedSessionRevision: approval.suspendedSessionRevision });
|
|
119
|
+
first.bindPrompt({ approvalId: prepared.record.approvalId, transport: "telegram", transportChatId: "acceptance-probe", transportMessageId: "acceptance-message" });
|
|
120
|
+
const claimed = first.decide({
|
|
121
|
+
approvalId: prepared.record.approvalId, decisionToken: prepared.decisionToken, decision: "approve", requesterId: "acceptance-probe",
|
|
122
|
+
transport: "telegram", transportUserId: "acceptance-probe", transportChatId: "acceptance-probe", transportMessageId: "acceptance-message",
|
|
123
|
+
sessionKey: "acceptance:attempted-recovery", ownerId,
|
|
124
|
+
});
|
|
125
|
+
const attempted = first.markAttempted({ approvalId: claimed.approvalId, ownerId, epoch: claimed.epoch });
|
|
126
|
+
first.close();
|
|
127
|
+
const attemptedRecordDigest = sha256(JSON.stringify(attempted));
|
|
128
|
+
const reopened = (0, approval_store_1.openApprovalStore)({ databasePath, now: () => instant });
|
|
129
|
+
const durableAttempted = reopened.read(attempted.approvalId);
|
|
130
|
+
if (!durableAttempted || durableAttempted.state !== "attempted") {
|
|
131
|
+
reopened.close();
|
|
132
|
+
throw new Error("attempted recovery record was not durable across reopen");
|
|
133
|
+
}
|
|
134
|
+
const recovered = (0, tool_approval_1.recoverAttemptedApproval)({ approvalStore: reopened, approvalId: durableAttempted.approvalId });
|
|
135
|
+
let retryCount = 0;
|
|
136
|
+
try {
|
|
137
|
+
(0, tool_approval_1.recoverAttemptedApproval)({ approvalStore: reopened, approvalId: durableAttempted.approvalId });
|
|
138
|
+
retryCount += 1;
|
|
139
|
+
}
|
|
140
|
+
catch { /* expected */ }
|
|
141
|
+
reopened.close();
|
|
142
|
+
return { observed: recovered.state === "attempted_indeterminate", retryCount, reopened: true, attemptedRecordDigest, recoveredRecordDigest: sha256(JSON.stringify(recovered)) };
|
|
143
|
+
}
|
|
144
|
+
async function executeSanctuaryInteractiveEngine(raw, deps) {
|
|
145
|
+
const payload = object(raw, "interactive runtime payload");
|
|
146
|
+
exactKeys(payload, ["operation", "label", "scenarioHandleDigest"], "interactive runtime payload");
|
|
147
|
+
const operation = payload.operation;
|
|
148
|
+
const label = payload.label;
|
|
149
|
+
const scenarioHandleDigest = payload.scenarioHandleDigest;
|
|
150
|
+
if (typeof scenarioHandleDigest !== "string" || !SHA256.test(scenarioHandleDigest)
|
|
151
|
+
|| !["unit-16k-timeout-stale", "unit-16l-duplicate-callback", "unit-16m-restart-continuation"].includes(label)
|
|
152
|
+
|| !["drive_timeout_stale", "drive_duplicate_callbacks", "prepare_restart_continuation", "reconcile_restart_continuation"].includes(String(operation))
|
|
153
|
+
|| (operation === "drive_timeout_stale") !== (label === "unit-16k-timeout-stale")
|
|
154
|
+
|| (operation === "drive_duplicate_callbacks") !== (label === "unit-16l-duplicate-callback"))
|
|
155
|
+
throw new Error("interactive runtime operation binding is invalid");
|
|
156
|
+
const before = exactApproval(deps.readApprovals(scenarioHandleDigest));
|
|
157
|
+
if (!before.approval.suspendedSessionRevision)
|
|
158
|
+
throw new Error("interactive production approval checkpoint is unavailable");
|
|
159
|
+
if (operation === "drive_timeout_stale") {
|
|
160
|
+
if (before.approval.state === "proposed") {
|
|
161
|
+
exactPending(deps.readPending(), before.approval.approvalId);
|
|
162
|
+
return { state: "waiting" };
|
|
163
|
+
}
|
|
164
|
+
if (before.approval.state !== "expired" || before.approval.epoch !== 0)
|
|
165
|
+
throw new Error("timeout stale approval is not expired without a claim");
|
|
166
|
+
const captured = exactTerminalTombstone(deps.readPending(), before.approval.approvalId);
|
|
167
|
+
const session = await deps.createSession();
|
|
168
|
+
try {
|
|
169
|
+
const queryId = (0, node_crypto_1.randomBytes)(18).toString("base64url");
|
|
170
|
+
const result = await session.handle({ callbackData: captured.approveCallbackData, queryId, messageId: captured.messageId });
|
|
171
|
+
const after = exactApproval(deps.readApprovals(scenarioHandleDigest));
|
|
172
|
+
const promptTerminal = !session.pendingApprovalIds().includes(before.approval.approvalId);
|
|
173
|
+
if (!result.handled || result.accepted || result.reason !== "stale_callback" || after.approval.state !== "expired" || after.approval.epoch !== 0 || !promptTerminal) {
|
|
174
|
+
throw new Error("timeout stale callback proof failed");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
schemaVersion: "sanctuary-timeout-stale-driver-receipt-v1", phase: "complete", label, scenarioHandleDigest,
|
|
178
|
+
approvalIdDigest: sha256(before.approval.approvalId), checkpointDigest: before.approval.checkpointDigest,
|
|
179
|
+
suspendedSessionRevisionDigest: sha256(before.approval.suspendedSessionRevision), approvalEpochBefore: before.approval.epoch,
|
|
180
|
+
callbackAttempts: 1, distinctQueryCount: 1, callbackDataDigest: sha256(captured.approveCallbackData),
|
|
181
|
+
settledCount: 1, claimCount: 0, mutationCount: 0, staleAcknowledged: true, promptTerminal,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
session.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (before.approval.state !== "proposed")
|
|
189
|
+
throw new Error("interactive production approval is not currently proposed");
|
|
190
|
+
const pending = exactPending(deps.readPending(), before.approval.approvalId);
|
|
191
|
+
const common = { approvalIdDigest: sha256(before.approval.approvalId), checkpointDigest: before.approval.checkpointDigest, suspendedSessionRevisionDigest: sha256(before.approval.suspendedSessionRevision), approvalEpochBefore: before.approval.epoch };
|
|
192
|
+
if (operation === "prepare_restart_continuation") {
|
|
193
|
+
const recovery = deps.proveIndeterminateRecovery(before.approval, scenarioHandleDigest);
|
|
194
|
+
if (!recovery.observed || recovery.retryCount !== 0 || !recovery.reopened
|
|
195
|
+
|| !SHA256.test(recovery.attemptedRecordDigest) || !SHA256.test(recovery.recoveredRecordDigest)
|
|
196
|
+
|| recovery.attemptedRecordDigest === recovery.recoveredRecordDigest)
|
|
197
|
+
throw new Error("isolated attempted recovery proof failed");
|
|
198
|
+
return {
|
|
199
|
+
schemaVersion: "sanctuary-interactive-driver-receipt-v2", phase: "prepared", label, scenarioHandleDigest, ...common,
|
|
200
|
+
pendingDigestBefore: pendingDigest(pending), indeterminateRecoveryObserved: true,
|
|
201
|
+
attemptedRecoveryReopened: true, attemptedRecordDigest: recovery.attemptedRecordDigest, recoveredRecordDigest: recovery.recoveredRecordDigest,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const session = await deps.createSession();
|
|
205
|
+
try {
|
|
206
|
+
if (operation === "drive_duplicate_callbacks") {
|
|
207
|
+
const queryIds = [(0, node_crypto_1.randomBytes)(18).toString("base64url"), (0, node_crypto_1.randomBytes)(18).toString("base64url")];
|
|
208
|
+
let arrivals = 0;
|
|
209
|
+
let release;
|
|
210
|
+
const barrier = new Promise((resolve) => { release = resolve; });
|
|
211
|
+
const invoke = async (queryId) => { arrivals += 1; if (arrivals === 2)
|
|
212
|
+
release(); await barrier; return session.handle({ callbackData: pending.approveCallbackData, queryId, messageId: pending.messageId }); };
|
|
213
|
+
const results = await Promise.all(queryIds.map(invoke));
|
|
214
|
+
const after = exactApproval(deps.readApprovals(scenarioHandleDigest));
|
|
215
|
+
const stale = await session.handle({ callbackData: pending.approveCallbackData, queryId: (0, node_crypto_1.randomBytes)(18).toString("base64url"), messageId: pending.messageId });
|
|
216
|
+
return { schemaVersion: "sanctuary-interactive-driver-receipt-v2", phase: "complete", label, scenarioHandleDigest, ...common, callbackAttempts: 2, distinctQueryCount: new Set(queryIds).size, callbackDataDigest: sha256(pending.approveCallbackData), barrierObserved: arrivals === 2, settledCount: results.filter(({ handled }) => handled).length, claimCount: after.approval.epoch === before.approval.epoch + 1 ? 1 : 0, mutationCount: after.approval.state === "succeeded" ? 1 : 0, staleReplayAttempts: 1, staleReplaySettled: stale.handled, staleReplayMutationCount: 0, promptTerminal: !session.pendingApprovalIds().includes(before.approval.approvalId), writeCredentialObserved: deps.writeCredentialObserved() };
|
|
217
|
+
}
|
|
218
|
+
const result = await session.handle({ callbackData: pending.approveCallbackData, queryId: (0, node_crypto_1.randomBytes)(18).toString("base64url"), messageId: pending.messageId });
|
|
219
|
+
const after = exactApproval(deps.readApprovals(scenarioHandleDigest));
|
|
220
|
+
if (!after.continuation || after.approval.state !== "succeeded")
|
|
221
|
+
throw new Error("restart continuation did not complete");
|
|
222
|
+
return { approvalEpochAfterRestart: before.approval.epoch, continuationEpochAfter: after.continuation.continuationEpoch, pendingDigestAfter: pendingDigest(pending), pendingRestored: true, callbackAttempts: 1, mutationCount: result.accepted ? 1 : 0, indeterminateRetryCount: 0 };
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
session.close();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function createSanctuaryInteractiveControl(options) {
|
|
229
|
+
const socketPath = path.join(options.agentRoot, "state", "acceptance", "telegram-control.sock");
|
|
230
|
+
const runRequest = options.runRequest ?? (async (operation) => operation());
|
|
231
|
+
let server;
|
|
232
|
+
let updateId = 2_100_000_000;
|
|
233
|
+
return {
|
|
234
|
+
socketPath,
|
|
235
|
+
async start() {
|
|
236
|
+
if (server)
|
|
237
|
+
return;
|
|
238
|
+
(0, node_fs_1.mkdirSync)(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
239
|
+
if ((0, node_fs_1.existsSync)(socketPath))
|
|
240
|
+
(0, node_fs_1.unlinkSync)(socketPath);
|
|
241
|
+
server = (0, node_net_1.createServer)({ allowHalfOpen: true }, (connection) => {
|
|
242
|
+
let raw = "";
|
|
243
|
+
connection.setEncoding("utf8");
|
|
244
|
+
connection.on("error", () => undefined);
|
|
245
|
+
connection.on("data", (chunk) => { raw += chunk; if (Buffer.byteLength(raw) > MAX_CONTROL_REQUEST)
|
|
246
|
+
connection.destroy(); });
|
|
247
|
+
connection.on("end", () => {
|
|
248
|
+
void runRequest(async () => {
|
|
249
|
+
try {
|
|
250
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_interactive_control_request", message: "Sanctuary interactive control request received", meta: { bytes: Buffer.byteLength(raw) } });
|
|
251
|
+
const parsed = object(JSON.parse(raw), "interactive control request");
|
|
252
|
+
if (parsed.operation === "interactive_runtime_ready") {
|
|
253
|
+
exactKeys(parsed, ["operation", "label", "scenarioHandleDigest"], "interactive readiness request");
|
|
254
|
+
if (parsed.label !== "unit-16m-restart-continuation" || typeof parsed.scenarioHandleDigest !== "string" || !SHA256.test(parsed.scenarioHandleDigest))
|
|
255
|
+
throw new Error("interactive readiness binding is invalid");
|
|
256
|
+
connection.end(`${JSON.stringify({ ok: true, result: { ready: true } })}\n`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const result = await executeSanctuaryInteractiveEngine(parsed, {
|
|
260
|
+
agentRoot: options.agentRoot,
|
|
261
|
+
readApprovals: (digest) => (0, approval_store_1.readApprovalsByScenarioHandleDigest)(path.join(options.agentRoot, "state", "approvals", "approvals.sqlite"), digest),
|
|
262
|
+
readPending: () => new telegram_client_1.FileTelegramPendingApprovalStore(path.join(options.agentRoot, "state", "approvals", "telegram-pending.json")).load(),
|
|
263
|
+
createSession: async () => ({
|
|
264
|
+
handle: ({ callbackData, queryId, messageId }) => runRequest(() => options.transport.handleUpdate({
|
|
265
|
+
update_id: updateId++,
|
|
266
|
+
callback_query: {
|
|
267
|
+
id: queryId,
|
|
268
|
+
from: { id: Number(options.authorizedUserId) },
|
|
269
|
+
data: callbackData,
|
|
270
|
+
message: {
|
|
271
|
+
message_id: Number(messageId),
|
|
272
|
+
chat: { id: Number(options.authorizedChatId) },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
})),
|
|
276
|
+
pendingApprovalIds: () => options.transport.listPendingDeliveries().map(({ approvalId }) => approvalId),
|
|
277
|
+
close: () => undefined,
|
|
278
|
+
}),
|
|
279
|
+
proveIndeterminateRecovery: (approval, digest) => proveSanctuaryAttemptedRecoveryWithoutRetry(options.agentRoot, digest, approval),
|
|
280
|
+
writeCredentialObserved: () => /credential|api[_-]?key|token|secret/iu.test(raw),
|
|
281
|
+
});
|
|
282
|
+
connection.end(`${JSON.stringify({ ok: true, result })}\n`);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
connection.end(`${JSON.stringify({ ok: false, error: "interactive runtime operation failed" })}\n`);
|
|
286
|
+
}
|
|
287
|
+
}).catch(() => { if (!connection.destroyed)
|
|
288
|
+
connection.end(`${JSON.stringify({ ok: false, error: "interactive runtime operation failed" })}\n`); });
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(socketPath, () => { server.off("error", reject); (0, node_fs_1.chmodSync)(socketPath, 0o600); resolve(); }); });
|
|
292
|
+
},
|
|
293
|
+
async stop() { const active = server; server = undefined; if (active)
|
|
294
|
+
await new Promise((resolve) => active.close(() => resolve())); if ((0, node_fs_1.existsSync)(socketPath))
|
|
295
|
+
(0, node_fs_1.unlinkSync)(socketPath); },
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
async function sanctuaryInteractiveControlReady(socketPath, timeoutMs = 1_000) {
|
|
299
|
+
return new Promise((resolve) => {
|
|
300
|
+
const socket = (0, node_net_1.createConnection)(socketPath);
|
|
301
|
+
const finish = (ready) => { socket.destroy(); resolve(ready); };
|
|
302
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
303
|
+
socket.once("connect", () => finish(true));
|
|
304
|
+
socket.once("error", () => finish(false));
|
|
305
|
+
});
|
|
306
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runWithSanctuaryToolReceiptCollection = runWithSanctuaryToolReceiptCollection;
|
|
37
|
+
exports.createSanctuaryToolContext = createSanctuaryToolContext;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const node_crypto_1 = require("node:crypto");
|
|
41
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
42
|
+
const identity_1 = require("../heart/identity");
|
|
43
|
+
const runtime_credentials_1 = require("../heart/runtime-credentials");
|
|
44
|
+
const unraid_restart_1 = require("../repertoire/unraid-restart");
|
|
45
|
+
const unraid_client_1 = require("../repertoire/unraid-client");
|
|
46
|
+
const tools_unraid_1 = require("../repertoire/tools-unraid");
|
|
47
|
+
const runtime_1 = require("../nerves/runtime");
|
|
48
|
+
const sanctuary_acceptance_marker_1 = require("../heart/daemon/sanctuary-acceptance-marker");
|
|
49
|
+
const sanctuary_grounding_1 = require("./sanctuary-grounding");
|
|
50
|
+
const sanctuaryToolReceipts = new node_async_hooks_1.AsyncLocalStorage();
|
|
51
|
+
const sanctuaryToolGroundings = new node_async_hooks_1.AsyncLocalStorage();
|
|
52
|
+
const acceptanceLedgerTails = new Map();
|
|
53
|
+
async function runWithSanctuaryToolReceiptCollection(operation, observer) {
|
|
54
|
+
const digests = observer?.toolResultDigests ?? [];
|
|
55
|
+
const groundings = observer?.toolGroundings ?? [];
|
|
56
|
+
const result = await sanctuaryToolReceipts.run(digests, () => sanctuaryToolGroundings.run(groundings, operation));
|
|
57
|
+
return { result, toolResultDigests: [...digests], ...(groundings.length > 0 ? { toolGroundings: structuredClone(groundings) } : {}) };
|
|
58
|
+
}
|
|
59
|
+
function collectToolResult(result, toolName) {
|
|
60
|
+
const digest = (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(result)).digest("hex");
|
|
61
|
+
sanctuaryToolReceipts.getStore()?.push(digest);
|
|
62
|
+
let facts = null;
|
|
63
|
+
try {
|
|
64
|
+
facts = toolName ? (0, sanctuary_grounding_1.projectSanctuaryGrounding)(toolName, result) : null;
|
|
65
|
+
}
|
|
66
|
+
catch { /* validated tool adapters own malformed-result failure */ }
|
|
67
|
+
if (!facts || (toolName !== "unraid_get_system" && toolName !== "unraid_get_storage"))
|
|
68
|
+
return { resultDigest: digest };
|
|
69
|
+
const groundingDigest = (0, sanctuary_grounding_1.sanctuaryGroundingDigest)(facts);
|
|
70
|
+
const envelope = result;
|
|
71
|
+
const sourceIdentityDigest = envelope?.data?.sourceIdentityDigest;
|
|
72
|
+
if (typeof sourceIdentityDigest !== "string" || !/^[0-9a-f]{64}$/u.test(sourceIdentityDigest))
|
|
73
|
+
throw new Error("Sanctuary grounding source identity is invalid");
|
|
74
|
+
const verifiedSourceIdentityDigest = sourceIdentityDigest;
|
|
75
|
+
const observedAt = new Date().toISOString();
|
|
76
|
+
sanctuaryToolGroundings.getStore()?.push({ toolName, resultDigest: digest, groundingDigest, sourceIdentityDigest: verifiedSourceIdentityDigest, observedAt, facts });
|
|
77
|
+
return { resultDigest: digest, groundingDigest, sourceIdentityDigest: verifiedSourceIdentityDigest, observedAt };
|
|
78
|
+
}
|
|
79
|
+
function machineConfig(agentName) {
|
|
80
|
+
const result = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agentName);
|
|
81
|
+
if (!result.ok)
|
|
82
|
+
throw new Error(`Sanctuary machine runtime config is ${result.reason}`);
|
|
83
|
+
return result.config;
|
|
84
|
+
}
|
|
85
|
+
function required(config, field) {
|
|
86
|
+
const value = config[field];
|
|
87
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
88
|
+
throw new Error(`Sanctuary ${field} is missing`);
|
|
89
|
+
return value.trim();
|
|
90
|
+
}
|
|
91
|
+
function persistAttempt(filePath, attempt) {
|
|
92
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
93
|
+
const temporary = `${filePath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
|
|
94
|
+
fs.writeFileSync(temporary, `${JSON.stringify(attempt)}\n`, { mode: 0o600 });
|
|
95
|
+
fs.renameSync(temporary, filePath);
|
|
96
|
+
}
|
|
97
|
+
async function appendAcceptanceAttempt(agentName, attempt) {
|
|
98
|
+
if (!attempt.scenarioHandleDigest)
|
|
99
|
+
return;
|
|
100
|
+
const filePath = path.join((0, identity_1.getAgentRoot)(agentName), "state", "acceptance", "restart-attempts.ndjson");
|
|
101
|
+
const previous = acceptanceLedgerTails.get(filePath) ?? Promise.resolve();
|
|
102
|
+
const current = previous.catch(() => undefined).then(() => {
|
|
103
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
104
|
+
let existing = [];
|
|
105
|
+
try {
|
|
106
|
+
if (fs.statSync(filePath).size > 4 * 1024 * 1024)
|
|
107
|
+
throw new Error("restart ledger exceeds its bound");
|
|
108
|
+
existing = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
|
|
109
|
+
if (existing.length > 500 || existing.some((line) => Buffer.byteLength(line) > 8 * 1024))
|
|
110
|
+
throw new Error("restart ledger rows exceed their bound");
|
|
111
|
+
for (const line of existing) {
|
|
112
|
+
const value = JSON.parse(line);
|
|
113
|
+
if (!value || typeof value !== "object" || !value.container || typeof value.container.id !== "string" || !value.container.id || value.container.id.length > 128
|
|
114
|
+
|| typeof value.container.name !== "string" || !value.container.name || value.container.name.length > 128
|
|
115
|
+
|| !["attempt_not_started", "attempting", "succeeded", "attempted_or_indeterminate"].includes(String(value.state))
|
|
116
|
+
|| typeof value.observedAt !== "string" || !Number.isFinite(Date.parse(value.observedAt)) || new Date(Date.parse(value.observedAt)).toISOString() !== value.observedAt
|
|
117
|
+
|| !/^[0-9a-f]{64}$/u.test(String(value.actionDigest)) || !/^[0-9a-f]{64}$/u.test(String(value.argumentDigest))
|
|
118
|
+
|| !/^[0-9a-f]{64}$/u.test(String(value.scenarioHandleDigest)) || typeof value.approvalId !== "string" || !value.approvalId || value.approvalId.length > 128
|
|
119
|
+
|| typeof value.attemptId !== "string" || !value.attemptId || value.attemptId.length > 128 || typeof value.mutationAcknowledged !== "boolean"
|
|
120
|
+
|| (value.afterState !== null && (typeof value.afterState !== "string" || value.afterState.length > 64)))
|
|
121
|
+
throw new Error("restart ledger row is invalid");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code !== "ENOENT")
|
|
126
|
+
throw new Error("Sanctuary acceptance restart ledger is corrupt", { cause: error });
|
|
127
|
+
}
|
|
128
|
+
const entries = [...existing, JSON.stringify(attempt)].slice(-500);
|
|
129
|
+
const temporary = `${filePath}.tmp-${process.pid}-${(0, node_crypto_1.randomUUID)()}`;
|
|
130
|
+
fs.writeFileSync(temporary, `${entries.join("\n")}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
131
|
+
const handle = fs.openSync(temporary, "r");
|
|
132
|
+
try {
|
|
133
|
+
fs.fsyncSync(handle);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
fs.closeSync(handle);
|
|
137
|
+
}
|
|
138
|
+
fs.renameSync(temporary, filePath);
|
|
139
|
+
fs.chmodSync(filePath, 0o600);
|
|
140
|
+
const directory = fs.openSync(path.dirname(filePath), "r");
|
|
141
|
+
try {
|
|
142
|
+
fs.fsyncSync(directory);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
fs.closeSync(directory);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
acceptanceLedgerTails.set(filePath, current);
|
|
149
|
+
try {
|
|
150
|
+
await current;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (acceptanceLedgerTails.get(filePath) === current)
|
|
154
|
+
acceptanceLedgerTails.delete(filePath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function createSanctuaryToolContext(agentName) {
|
|
158
|
+
(0, runtime_1.emitNervesEvent)({
|
|
159
|
+
component: "senses",
|
|
160
|
+
event: "senses.sanctuary_runtime_create",
|
|
161
|
+
message: "creating typed Sanctuary tool context",
|
|
162
|
+
meta: { agentName },
|
|
163
|
+
});
|
|
164
|
+
const initial = machineConfig(agentName);
|
|
165
|
+
const endpoint = required(initial, "unraidGraphqlUrl");
|
|
166
|
+
const readClient = new unraid_client_1.UnraidClient({ endpoint, apiKey: required(initial, "unraidReadApiKey") });
|
|
167
|
+
const reads = (0, tools_unraid_1.createUnraidReadTools)(readClient);
|
|
168
|
+
const acceptanceRead = (toolName, read) => async (...args) => {
|
|
169
|
+
try {
|
|
170
|
+
const result = await read(...args);
|
|
171
|
+
const receipt = collectToolResult(result, toolName);
|
|
172
|
+
(0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_read_receipt", message: "Sanctuary live read completed", meta: { toolName, success: true, ...receipt, ...(0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(agentName) } });
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
(0, runtime_1.emitNervesEvent)({ level: "error", component: "senses", event: "senses.sanctuary_read_receipt_error", message: "Sanctuary live read failed", meta: { toolName, success: false, category: error instanceof Error ? error.name : "unknown", ...(0, sanctuary_acceptance_marker_1.sanctuaryAcceptanceEventMeta)(agentName) } });
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const restart = (0, unraid_restart_1.createApprovedUnraidRestartExecutor)({
|
|
181
|
+
endpoint,
|
|
182
|
+
listContainers: reads.listContainers,
|
|
183
|
+
loadWriteApiKey: async () => required(machineConfig(agentName), "unraidWriteApiKey"),
|
|
184
|
+
persistAttempt: async (attempt) => {
|
|
185
|
+
persistAttempt(path.join((0, identity_1.getAgentRoot)(agentName), "state", "approvals", "unraid-restart-attempt.json"), attempt);
|
|
186
|
+
await appendAcceptanceAttempt(agentName, attempt);
|
|
187
|
+
},
|
|
188
|
+
acceptanceScenarioHandleDigest: () => (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(agentName)?.scenarioHandleDigest,
|
|
189
|
+
acceptanceApproval: sanctuary_acceptance_marker_1.readSanctuaryAcceptanceApproval,
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
sanctuary: {
|
|
193
|
+
listContainers: acceptanceRead("unraid_list_containers", reads.listContainers),
|
|
194
|
+
getContainerLogs: acceptanceRead("unraid_get_container_logs", reads.getContainerLogs),
|
|
195
|
+
getStorage: acceptanceRead("unraid_get_storage", reads.getStorage),
|
|
196
|
+
getDisks: acceptanceRead("unraid_get_disks", reads.getDisks),
|
|
197
|
+
getNotifications: acceptanceRead("unraid_get_notifications", reads.getNotifications),
|
|
198
|
+
getSystem: acceptanceRead("unraid_get_system", reads.getSystem),
|
|
199
|
+
restartContainer: async (args) => {
|
|
200
|
+
const result = await restart(args);
|
|
201
|
+
collectToolResult(result);
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -237,6 +237,7 @@ async function runSenseTurn(options) {
|
|
|
237
237
|
const sessPath = path.join(sessionDir, `${(0, config_1.sanitizeKey)(sessionKey)}.json`);
|
|
238
238
|
const runWithLease = options._withSessionTurnLease ?? session_transaction_1.withSessionTurnLease;
|
|
239
239
|
return runWithLease(sessPath, async (sessionTurnLease) => {
|
|
240
|
+
const baseSessionRevision = (0, session_transaction_1.readSessionTransaction)(sessPath, sessionTurnLease).revision;
|
|
240
241
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
241
242
|
let sessionState = existing?.state;
|
|
242
243
|
let persistPromise;
|
|
@@ -253,6 +254,8 @@ async function runSenseTurn(options) {
|
|
|
253
254
|
let terminalDeliveryKind = "text";
|
|
254
255
|
const deliveries = [];
|
|
255
256
|
const deliveryFailures = [];
|
|
257
|
+
let providerInvocationCount = 0;
|
|
258
|
+
let toolInvocationCount = 0;
|
|
256
259
|
const commitResponseText = (text) => {
|
|
257
260
|
const cleaned = stripThinkBlocks(text);
|
|
258
261
|
/* v8 ignore next -- deliverPending strips first; this is a defensive direct-call guard @preserve */
|
|
@@ -292,11 +295,13 @@ async function runSenseTurn(options) {
|
|
|
292
295
|
/* v8 ignore start — no-op callback stubs; only onTextChunk does real work (covered via mock) */
|
|
293
296
|
const callbacks = {
|
|
294
297
|
settleOutputMode: "retractable_buffer",
|
|
295
|
-
onModelStart: () => {
|
|
298
|
+
onModelStart: () => { providerInvocationCount += 1; if (options.turnMetricsObserver)
|
|
299
|
+
options.turnMetricsObserver.providerInvocationCount += 1; },
|
|
296
300
|
onModelStreamStart: () => { },
|
|
297
301
|
onTextChunk: (chunk) => { pendingResponseText += chunk; },
|
|
298
302
|
onReasoningChunk: () => { },
|
|
299
|
-
onToolStart: () => {
|
|
303
|
+
onToolStart: () => { toolInvocationCount += 1; if (options.turnMetricsObserver)
|
|
304
|
+
options.turnMetricsObserver.toolInvocationCount += 1; },
|
|
300
305
|
onToolEnd: (name, _summary, success) => {
|
|
301
306
|
if (name === "settle" && success)
|
|
302
307
|
terminalDeliveryKind = "settle";
|
|
@@ -338,6 +343,7 @@ async function runSenseTurn(options) {
|
|
|
338
343
|
drainPending: pending_1.drainPending,
|
|
339
344
|
runAgentOptions: {
|
|
340
345
|
mcpManager,
|
|
346
|
+
...(options.approvalCoordinatorFactory ? { approvalCoordinator: options.approvalCoordinatorFactory({ sessionPath: sessPath, baseSessionRevision }) } : {}),
|
|
341
347
|
...(options.latencyMode === "live" ? { skipKeptNotes: true } : {}),
|
|
342
348
|
toolContext: {
|
|
343
349
|
signin: async () => undefined,
|
|
@@ -364,6 +370,8 @@ async function runSenseTurn(options) {
|
|
|
364
370
|
ponderDeferred: false,
|
|
365
371
|
deliveries,
|
|
366
372
|
deliveryFailures,
|
|
373
|
+
providerInvocationCount,
|
|
374
|
+
toolInvocationCount,
|
|
367
375
|
};
|
|
368
376
|
}
|
|
369
377
|
if (persistPromise)
|
|
@@ -419,6 +427,6 @@ async function runSenseTurn(options) {
|
|
|
419
427
|
message: "shared turn runner complete",
|
|
420
428
|
meta: { agentName, channel, sessionKey, friendId, ponderDeferred, responseLength: finalResponse.length },
|
|
421
429
|
});
|
|
422
|
-
return { response: finalResponse, ponderDeferred, deliveries, deliveryFailures };
|
|
430
|
+
return { response: finalResponse, ponderDeferred, deliveries, deliveryFailures, providerInvocationCount, toolInvocationCount };
|
|
423
431
|
});
|
|
424
432
|
}
|