@haaaiawd/second-nature 0.1.39 → 0.1.41
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/index.js +1270 -1270
- package/openclaw.plugin.json +29 -29
- package/package.json +55 -55
- package/runtime/cli/commands/connector-init.js +11 -4
- package/runtime/cli/index.js +6 -1
- package/runtime/cli/ops/heartbeat-surface.d.ts +75 -75
- package/runtime/cli/ops/heartbeat-surface.js +97 -97
- package/runtime/cli/ops/ops-router.js +1428 -1428
- package/runtime/cli/ops/workspace-heartbeat-runner.js +236 -236
- package/runtime/connectors/services/connector-executor-adapter.js +192 -41
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +1 -1
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +2 -1
- package/runtime/core/second-nature/guidance/apply-guidance.d.ts +12 -12
- package/runtime/core/second-nature/guidance/apply-guidance.js +15 -15
- package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +50 -50
- package/runtime/core/second-nature/guidance/user-reply-continuity.js +89 -89
- package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
- package/runtime/core/second-nature/runtime/service-entry.d.ts +39 -39
- package/runtime/core/second-nature/runtime/service-entry.js +44 -44
- package/runtime/dream/dream-engine.d.ts +14 -14
- package/runtime/dream/dream-engine.js +306 -306
- package/runtime/dream/dream-input-loader.d.ts +37 -37
- package/runtime/dream/dream-input-loader.js +150 -150
- package/runtime/dream/dream-scheduler.d.ts +75 -75
- package/runtime/dream/dream-scheduler.js +131 -131
- package/runtime/dream/index.d.ts +16 -16
- package/runtime/dream/index.js +14 -14
- package/runtime/dream/insight-extractor.d.ts +32 -32
- package/runtime/dream/insight-extractor.js +135 -135
- package/runtime/dream/memory-consolidator.d.ts +45 -45
- package/runtime/dream/memory-consolidator.js +140 -140
- package/runtime/dream/narrative-update-proposal.d.ts +34 -34
- package/runtime/dream/narrative-update-proposal.js +83 -83
- package/runtime/dream/output-validator.d.ts +20 -20
- package/runtime/dream/output-validator.js +110 -110
- package/runtime/dream/redaction-gate.d.ts +31 -31
- package/runtime/dream/redaction-gate.js +109 -109
- package/runtime/dream/relationship-update-proposal.d.ts +27 -27
- package/runtime/dream/relationship-update-proposal.js +119 -119
- package/runtime/dream/sampler.d.ts +30 -30
- package/runtime/dream/sampler.js +65 -65
- package/runtime/dream/types.d.ts +187 -187
- package/runtime/dream/types.js +11 -11
- package/runtime/guidance/fallback.js +20 -20
- package/runtime/guidance/guidance-assembler.js +76 -76
- package/runtime/guidance/output-guard.d.ts +13 -13
- package/runtime/guidance/output-guard.js +53 -53
- package/runtime/guidance/template-registry.d.ts +20 -20
- package/runtime/guidance/template-registry.js +93 -93
- package/runtime/guidance/types.d.ts +98 -98
- package/runtime/observability/projections/guidance-audit.js +38 -38
|
@@ -1,1428 +1,1428 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared ops command dispatch for CLI + tool surfaces (T1.1.3, T1.2.2).
|
|
3
|
-
*
|
|
4
|
-
* v7 additions (T-ROS.C.1): self_health, tool_affordance, connector_test --wet,
|
|
5
|
-
* heartbeat_digest, narrative:diff, timeline, restore, runtime_secret_bootstrap.
|
|
6
|
-
* All commands return RuntimeOpsEnvelope.
|
|
7
|
-
*/
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import fs from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import { heartbeatCheck, } from "./heartbeat-surface.js";
|
|
12
|
-
import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
|
|
13
|
-
import { probeHostCapability } from "../host-capability/probe-host-capability.js";
|
|
14
|
-
import { recordHostCapability } from "../host-capability/record-host-capability.js";
|
|
15
|
-
import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
|
|
16
|
-
import { connectorInit } from "../commands/connector-init.js";
|
|
17
|
-
import { connectorBehaviorAdd } from "../commands/connector-behavior.js";
|
|
18
|
-
import { connectorStatus, connectorTest } from "../commands/connector-status.js";
|
|
19
|
-
import { goalCommand } from "../commands/goal.js";
|
|
20
|
-
// v7 observability services (T-ROS.C.1)
|
|
21
|
-
import { getSelfHealthSnapshot, ensureMinimumProbes, } from "../../observability/services/self-health-snapshot.js";
|
|
22
|
-
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
23
|
-
import { queryNarrativeTimeline, queryNarrativeDiff, NarrativeVersionNotFoundError, } from "../../observability/services/narrative-timeline-query-service.js";
|
|
24
|
-
import { viewSecretAnchor, } from "../../observability/services/runtime-secret-anchor-view.js";
|
|
25
|
-
import { writeRestoreAudit, } from "../../observability/services/restore-audit-service.js";
|
|
26
|
-
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
27
|
-
// T-ROS.C.3: ManualRunDispatcher and its deps
|
|
28
|
-
import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
|
|
29
|
-
import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
30
|
-
import { createCapabilityProbeResultStore, createToolExperienceStore, } from "../../storage/services/tool-experience-store.js";
|
|
31
|
-
import { createWetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
|
|
32
|
-
import { CapabilityContractRegistryV7 } from "../../connectors/base/manifest-v7.js";
|
|
33
|
-
// v7 T-V7C.C.6: Dream scheduling deps for heartbeat_check quiet→dream auto-trigger
|
|
34
|
-
import { scheduleDream } from "../../dream/dream-scheduler.js";
|
|
35
|
-
import { createDreamInputLoader } from "../../dream/dream-input-loader.js";
|
|
36
|
-
import { createDiaryDreamStore } from "../../storage/services/diary-dream-store.js";
|
|
37
|
-
function coerceProbeOnlyFlag(input) {
|
|
38
|
-
const v = input?.probeOnly;
|
|
39
|
-
return v === true || v === "true" || v === 1 || v === "1";
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* v7 T-V7C.C.6: Build a minimal QuietDreamSchedulePort backed by the state DB.
|
|
43
|
-
* When a source-backed Quiet write completes, this port triggers Dream scheduling
|
|
44
|
-
* via the standard scheduleDream path (rules-only mode when no model port).
|
|
45
|
-
*/
|
|
46
|
-
function createQuietDreamSchedulePort(state) {
|
|
47
|
-
return {
|
|
48
|
-
async scheduleDream({ triggerKind, runId, traceId }) {
|
|
49
|
-
const dreamStore = createDiaryDreamStore(state);
|
|
50
|
-
const inputLoader = createDreamInputLoader({ database: state });
|
|
51
|
-
const statePort = {
|
|
52
|
-
async loadDreamInputs(query) {
|
|
53
|
-
return inputLoader.loadDreamInputs(query);
|
|
54
|
-
},
|
|
55
|
-
async writeDreamOutput(output) {
|
|
56
|
-
// Bridge: dream-engine emits dream/types DreamOutput; diary-dream-store expects shared/types.
|
|
57
|
-
// Structures are identical at runtime; TS strictness requires the cast.
|
|
58
|
-
await dreamStore.appendDreamOutput(output);
|
|
59
|
-
return { outputId: output.outputId, status: "acknowledged" };
|
|
60
|
-
},
|
|
61
|
-
async markDreamOutputLifecycle(input) {
|
|
62
|
-
// transitionDreamOutputLifecycle only accepts accepted|archived.
|
|
63
|
-
if (input.newStatus !== "accepted" && input.newStatus !== "archived") {
|
|
64
|
-
return { outputId: input.outputId, status: "degraded" };
|
|
65
|
-
}
|
|
66
|
-
await dreamStore.transitionDreamOutputLifecycle(input.outputId, input.newStatus);
|
|
67
|
-
return { outputId: input.outputId, status: "acknowledged" };
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
const result = await scheduleDream({
|
|
71
|
-
triggerKind,
|
|
72
|
-
runId,
|
|
73
|
-
traceId,
|
|
74
|
-
statePort,
|
|
75
|
-
windowKey: "quiet_completion",
|
|
76
|
-
});
|
|
77
|
-
return { status: result.status, reason: result.reason };
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
const SNAPSHOT_TABLE_BY_KIND = {
|
|
82
|
-
identity_profile: "identity_profile",
|
|
83
|
-
agent_goal: "agent_goal",
|
|
84
|
-
tool_experience: "tool_experience",
|
|
85
|
-
daily_diary: "daily_diary_index",
|
|
86
|
-
dream_output: "dream_output_index",
|
|
87
|
-
narrative_timeline: "narrative_timeline",
|
|
88
|
-
};
|
|
89
|
-
const DEFAULT_SNAPSHOT_KINDS = [
|
|
90
|
-
"identity_profile",
|
|
91
|
-
"agent_goal",
|
|
92
|
-
"tool_experience",
|
|
93
|
-
"daily_diary",
|
|
94
|
-
"dream_output",
|
|
95
|
-
"narrative_timeline",
|
|
96
|
-
];
|
|
97
|
-
function coerceRestorableKinds(value) {
|
|
98
|
-
if (!Array.isArray(value))
|
|
99
|
-
return undefined;
|
|
100
|
-
const valid = new Set(DEFAULT_SNAPSHOT_KINDS);
|
|
101
|
-
return value.filter((item) => typeof item === "string" && valid.has(item));
|
|
102
|
-
}
|
|
103
|
-
function tableExists(state, table) {
|
|
104
|
-
const result = state.sqlite.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`, [table]);
|
|
105
|
-
return result.length > 0 && result[0].values.length > 0;
|
|
106
|
-
}
|
|
107
|
-
function readRowsFromTable(state, table) {
|
|
108
|
-
const result = state.sqlite.exec(`SELECT * FROM ${table}`);
|
|
109
|
-
if (result.length === 0 || result[0].values.length === 0)
|
|
110
|
-
return [];
|
|
111
|
-
const columns = result[0].columns;
|
|
112
|
-
return result[0].values.map((row) => {
|
|
113
|
-
const out = {};
|
|
114
|
-
columns.forEach((column, index) => {
|
|
115
|
-
out[column] = row[index];
|
|
116
|
-
});
|
|
117
|
-
return out;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
function stringArray(value) {
|
|
121
|
-
return Array.isArray(value)
|
|
122
|
-
? value.filter((item) => typeof item === "string")
|
|
123
|
-
: [];
|
|
124
|
-
}
|
|
125
|
-
function textInput(input, key) {
|
|
126
|
-
const value = input?.[key];
|
|
127
|
-
if (typeof value !== "string")
|
|
128
|
-
return undefined;
|
|
129
|
-
const trimmed = value.trim();
|
|
130
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
131
|
-
}
|
|
132
|
-
function buildSnapshotNarrativeDelta(input, snapshotId, rowCounts) {
|
|
133
|
-
const explicit = input?.narrativeSnapshot &&
|
|
134
|
-
typeof input.narrativeSnapshot === "object" &&
|
|
135
|
-
!Array.isArray(input.narrativeSnapshot)
|
|
136
|
-
? input.narrativeSnapshot
|
|
137
|
-
: {};
|
|
138
|
-
const from = (key) => input?.[key] ?? explicit[key];
|
|
139
|
-
const sourceRefs = stringArray(from("sourceRefs"));
|
|
140
|
-
return {
|
|
141
|
-
focus: from("focus") ?? "workspace_state",
|
|
142
|
-
progress: from("progress") ??
|
|
143
|
-
`snapshot_captured:${Object.entries(rowCounts)
|
|
144
|
-
.map(([kind, count]) => `${kind}=${count}`)
|
|
145
|
-
.join(",")}`,
|
|
146
|
-
nextIntent: from("nextIntent") ?? "restore_ready",
|
|
147
|
-
toneSignal: from("toneSignal") ?? "system_maintenance",
|
|
148
|
-
acceptedGoalId: from("acceptedGoalId") ?? undefined,
|
|
149
|
-
sourceRefs: sourceRefs.length > 0
|
|
150
|
-
? sourceRefs
|
|
151
|
-
: [`restore_snapshot:${snapshotId}`, "runtime_ops:snapshot_capture"],
|
|
152
|
-
reasonCode: from("reasonCode") ?? "snapshot_captured",
|
|
153
|
-
summaryText: from("summaryText") ?? `Captured restore snapshot ${snapshotId}`,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
function hashNarrativeSnapshot(input) {
|
|
157
|
-
return createHash("sha256")
|
|
158
|
-
.update(JSON.stringify({
|
|
159
|
-
previousHash: input.previousHash,
|
|
160
|
-
snapshotId: input.snapshotId,
|
|
161
|
-
delta: input.delta,
|
|
162
|
-
createdAt: input.createdAt,
|
|
163
|
-
}))
|
|
164
|
-
.digest("hex");
|
|
165
|
-
}
|
|
166
|
-
function resolveManifestPath(manifestPath, workspaceRoot) {
|
|
167
|
-
if (path.isAbsolute(manifestPath))
|
|
168
|
-
return manifestPath;
|
|
169
|
-
return path.join(workspaceRoot ?? process.cwd(), manifestPath);
|
|
170
|
-
}
|
|
171
|
-
function registerConnectorForWetProbe(input) {
|
|
172
|
-
if (input.entry.manifestPath) {
|
|
173
|
-
try {
|
|
174
|
-
const manifestText = fs.readFileSync(resolveManifestPath(input.entry.manifestPath, input.workspaceRoot), "utf-8");
|
|
175
|
-
const parsed = JSON.parse(manifestText);
|
|
176
|
-
const registered = input.registryV7.register(parsed);
|
|
177
|
-
if (registered.ok && input.registryV7.hasCapability(input.entry.platformId, input.selectedCapabilityId)) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
// Non-v7 or YAML workspace manifests are projected below.
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
input.registryV7.register({
|
|
186
|
-
platformId: input.entry.platformId,
|
|
187
|
-
capabilities: input.entry.capabilities.map((capabilityId) => ({
|
|
188
|
-
capabilityId,
|
|
189
|
-
intent: capabilityId,
|
|
190
|
-
probeConfig: capabilityId === input.selectedCapabilityId && input.safeEndpoint
|
|
191
|
-
? {
|
|
192
|
-
safeEndpoint: input.safeEndpoint,
|
|
193
|
-
idempotencyClass: "read_only",
|
|
194
|
-
}
|
|
195
|
-
: undefined,
|
|
196
|
-
})),
|
|
197
|
-
channelPriority: ["runtime_ops"],
|
|
198
|
-
credentialTypes: ["runtime_ops_probe"],
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
async function captureRuntimeSnapshot(deps, input) {
|
|
202
|
-
const generatedAt = new Date().toISOString();
|
|
203
|
-
if (!deps.state || !deps.restoreSnapshotStore) {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
command: "snapshot:capture",
|
|
207
|
-
runtimeMode: "unavailable",
|
|
208
|
-
surfaceMode: "cli",
|
|
209
|
-
generatedAt,
|
|
210
|
-
error: {
|
|
211
|
-
code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
|
|
212
|
-
message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
|
|
213
|
-
nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
|
|
214
|
-
},
|
|
215
|
-
warnings: [],
|
|
216
|
-
sourceRefs: [],
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
const snapshotId = textInput(input, "snapshotId") ??
|
|
220
|
-
`snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
221
|
-
const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
|
|
222
|
-
const rowCounts = {};
|
|
223
|
-
const warnings = [];
|
|
224
|
-
for (const kind of requestedKinds) {
|
|
225
|
-
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
226
|
-
if (!tableExists(deps.state, table)) {
|
|
227
|
-
rowCounts[kind] = 0;
|
|
228
|
-
warnings.push(`table_missing:${kind}:${table}`);
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
rowCounts[kind] = readRowsFromTable(deps.state, table).length;
|
|
232
|
-
}
|
|
233
|
-
const historyStore = createHistoryDigestStore(deps.state);
|
|
234
|
-
const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
|
|
235
|
-
const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
|
|
236
|
-
const currentHash = hashNarrativeSnapshot({
|
|
237
|
-
previousHash,
|
|
238
|
-
snapshotId,
|
|
239
|
-
delta,
|
|
240
|
-
createdAt: generatedAt,
|
|
241
|
-
});
|
|
242
|
-
await historyStore.appendNarrativeTimeline({
|
|
243
|
-
timelineId: snapshotId,
|
|
244
|
-
entryType: "owner.override",
|
|
245
|
-
subjectId: textInput(input, "subjectId") ?? snapshotId,
|
|
246
|
-
delta,
|
|
247
|
-
previousHash,
|
|
248
|
-
currentHash,
|
|
249
|
-
createdAt: generatedAt,
|
|
250
|
-
});
|
|
251
|
-
const payload = {};
|
|
252
|
-
const capturedKinds = [];
|
|
253
|
-
for (const kind of requestedKinds) {
|
|
254
|
-
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
255
|
-
if (!tableExists(deps.state, table))
|
|
256
|
-
continue;
|
|
257
|
-
const rows = readRowsFromTable(deps.state, table);
|
|
258
|
-
rowCounts[kind] = rows.length;
|
|
259
|
-
if (rows.length > 0) {
|
|
260
|
-
payload[kind] = rows;
|
|
261
|
-
capturedKinds.push(kind);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
|
|
265
|
-
snapshotId,
|
|
266
|
-
entityWhitelist: requestedKinds,
|
|
267
|
-
payload,
|
|
268
|
-
capturedAt: generatedAt,
|
|
269
|
-
});
|
|
270
|
-
return {
|
|
271
|
-
ok: true,
|
|
272
|
-
command: "snapshot:capture",
|
|
273
|
-
runtimeMode: "workspace_full_runtime",
|
|
274
|
-
surfaceMode: "cli",
|
|
275
|
-
generatedAt,
|
|
276
|
-
data: {
|
|
277
|
-
snapshotId: snapshot.snapshotId,
|
|
278
|
-
capturedAt: snapshot.capturedAt,
|
|
279
|
-
entityWhitelist: snapshot.entityWhitelist,
|
|
280
|
-
capturedKinds,
|
|
281
|
-
rowCounts,
|
|
282
|
-
narrativeVersion: snapshotId,
|
|
283
|
-
},
|
|
284
|
-
warnings,
|
|
285
|
-
sourceRefs: [
|
|
286
|
-
"storage/services/restore-snapshot-store.ts",
|
|
287
|
-
"storage/services/history-digest-store.ts",
|
|
288
|
-
],
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
|
|
293
|
-
* Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
|
|
294
|
-
*/
|
|
295
|
-
function createStaticUnknownAdapter() {
|
|
296
|
-
const now = new Date().toISOString();
|
|
297
|
-
const unknownResult = (name) => ({
|
|
298
|
-
name,
|
|
299
|
-
verdict: "unknown",
|
|
300
|
-
observedAt: now,
|
|
301
|
-
reason: "static_local_probe_no_host_context",
|
|
302
|
-
evidenceRefs: [],
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
checkPluginLoad: () => unknownResult("plugin_load"),
|
|
306
|
-
checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
|
|
307
|
-
checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
|
|
308
|
-
checkDeliveryTarget: () => ({ status: "unknown", evidenceRefs: [] }),
|
|
309
|
-
checkAckDropBehavior: () => unknownResult("ack_drop"),
|
|
310
|
-
checkHookSupport: () => [],
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
export function createOpsRouter(deps) {
|
|
314
|
-
return {
|
|
315
|
-
heartbeatCheck: (input) => heartbeatCheck({
|
|
316
|
-
...input,
|
|
317
|
-
runtimeAvailable: input.runtimeAvailable ?? deps.runtimeAvailable,
|
|
318
|
-
readModels: input.readModels ?? deps.readModels,
|
|
319
|
-
runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
|
|
320
|
-
state: input.state ?? deps.state,
|
|
321
|
-
workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
|
|
322
|
-
connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
|
|
323
|
-
connectorRegistry: input
|
|
324
|
-
?.connectorRegistry ?? deps.connectorRegistry,
|
|
325
|
-
digestOpts: input.digestOpts,
|
|
326
|
-
dreamSchedulePort: input.dreamSchedulePort,
|
|
327
|
-
}),
|
|
328
|
-
async dispatch(command, input) {
|
|
329
|
-
if (command === "heartbeat_check") {
|
|
330
|
-
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
331
|
-
? input.runtimeAvailable
|
|
332
|
-
: deps.runtimeAvailable;
|
|
333
|
-
// v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
|
|
334
|
-
let affordanceMap;
|
|
335
|
-
if (deps.toolAffordancePort) {
|
|
336
|
-
try {
|
|
337
|
-
affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
|
|
338
|
-
}
|
|
339
|
-
catch {
|
|
340
|
-
// degrade gracefully; guard-layer will skip breaker check without affordanceMap
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
let experienceWriter;
|
|
344
|
-
if (deps.state) {
|
|
345
|
-
experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
|
|
346
|
-
}
|
|
347
|
-
// v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
|
|
348
|
-
let digestOpts;
|
|
349
|
-
if (deps.auditStore) {
|
|
350
|
-
digestOpts = {
|
|
351
|
-
assemblerDeps: {
|
|
352
|
-
auditStore: deps.auditStore,
|
|
353
|
-
...deps.heartbeatDigestDeps,
|
|
354
|
-
},
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
// v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
|
|
358
|
-
let dreamSchedulePort;
|
|
359
|
-
if (deps.state) {
|
|
360
|
-
dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
const result = await heartbeatCheck({
|
|
364
|
-
probeOnly: coerceProbeOnlyFlag(input),
|
|
365
|
-
runtimeAvailable,
|
|
366
|
-
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
|
|
367
|
-
typeof input.fakeControlPlanePassthrough === "object"
|
|
368
|
-
? input.fakeControlPlanePassthrough
|
|
369
|
-
: undefined,
|
|
370
|
-
readModels: input?.readModels ??
|
|
371
|
-
deps.readModels,
|
|
372
|
-
runtimeRecorder: input
|
|
373
|
-
?.runtimeRecorder ?? deps.runtimeRecorder,
|
|
374
|
-
state: input?.state ??
|
|
375
|
-
deps.state,
|
|
376
|
-
workspaceRoot: input
|
|
377
|
-
?.workspaceRoot ?? deps.workspaceRoot,
|
|
378
|
-
timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
|
|
379
|
-
sessionContext: typeof input?.sessionContext === "string"
|
|
380
|
-
? input.sessionContext
|
|
381
|
-
: undefined,
|
|
382
|
-
scopeHint: input?.scopeHint,
|
|
383
|
-
connectorExecutor: input
|
|
384
|
-
?.connectorExecutor ?? deps.connectorExecutor,
|
|
385
|
-
connectorRegistry: input
|
|
386
|
-
?.connectorRegistry ?? deps.connectorRegistry,
|
|
387
|
-
affordanceMap,
|
|
388
|
-
experienceWriter,
|
|
389
|
-
digestOpts,
|
|
390
|
-
dreamSchedulePort,
|
|
391
|
-
});
|
|
392
|
-
if (result.ok &&
|
|
393
|
-
result.surfaceMode === "workspace_full_runtime" &&
|
|
394
|
-
!coerceProbeOnlyFlag(input) &&
|
|
395
|
-
deps.state &&
|
|
396
|
-
deps.restoreSnapshotStore) {
|
|
397
|
-
try {
|
|
398
|
-
const capture = await captureRuntimeSnapshot(deps, {
|
|
399
|
-
snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
|
|
400
|
-
subjectId: result.decisionId ?? "heartbeat_check",
|
|
401
|
-
reasonCode: "heartbeat_check",
|
|
402
|
-
summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
|
|
403
|
-
focus: result.status,
|
|
404
|
-
progress: result.reasons.join(",") || "heartbeat_completed",
|
|
405
|
-
nextIntent: "continue_runtime_loop",
|
|
406
|
-
sourceRefs: result.decisionId
|
|
407
|
-
? [`heartbeat:${result.decisionId}`]
|
|
408
|
-
: ["heartbeat:runtime"],
|
|
409
|
-
});
|
|
410
|
-
if (capture.ok) {
|
|
411
|
-
result.reasons = [...result.reasons, "restore_snapshot_captured"];
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
416
|
-
result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
return result;
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
-
const envelope = {
|
|
424
|
-
ok: false,
|
|
425
|
-
command: "heartbeat_check",
|
|
426
|
-
runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
|
|
427
|
-
surfaceMode: "cli",
|
|
428
|
-
generatedAt: new Date().toISOString(),
|
|
429
|
-
error: {
|
|
430
|
-
code: "HEARTBEAT_CYCLE_EXCEPTION",
|
|
431
|
-
message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
|
|
432
|
-
nextStep: "check_logs_and_report",
|
|
433
|
-
},
|
|
434
|
-
warnings: [],
|
|
435
|
-
sourceRefs: [],
|
|
436
|
-
};
|
|
437
|
-
return envelope;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
if (command === "fallback") {
|
|
441
|
-
const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
|
|
442
|
-
if (!ref) {
|
|
443
|
-
return {
|
|
444
|
-
ok: false,
|
|
445
|
-
error: {
|
|
446
|
-
code: "MISSING_FALLBACK_REF",
|
|
447
|
-
message: "fallback requires args.ref (e.g. fallback:…)",
|
|
448
|
-
requiredUserInput: ["ref"],
|
|
449
|
-
nextStep: "reinvoke_with_ref",
|
|
450
|
-
},
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
if (!deps.readModels?.loadFallbackView) {
|
|
454
|
-
return {
|
|
455
|
-
ok: false,
|
|
456
|
-
error: {
|
|
457
|
-
code: "FALLBACK_READ_MODEL_UNAVAILABLE",
|
|
458
|
-
message: "Operator fallback view requires workspace read models",
|
|
459
|
-
requiredUserInput: ["ref"],
|
|
460
|
-
nextStep: "wire_read_models_into_ops_router",
|
|
461
|
-
},
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
return (async () => {
|
|
465
|
-
try {
|
|
466
|
-
const data = await showOperatorFallback(ref, deps.readModels);
|
|
467
|
-
return { ok: true, command: "fallback", data };
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
if (error instanceof OperatorFallbackNotFoundError) {
|
|
471
|
-
return {
|
|
472
|
-
ok: false,
|
|
473
|
-
command: "fallback",
|
|
474
|
-
error: {
|
|
475
|
-
code: error.code,
|
|
476
|
-
message: error.message,
|
|
477
|
-
requiredUserInput: ["ref"],
|
|
478
|
-
nextStep: "verify_fallback_ref_from_delivery_audit",
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
throw error;
|
|
483
|
-
}
|
|
484
|
-
})();
|
|
485
|
-
}
|
|
486
|
-
if (command === "capability_probe") {
|
|
487
|
-
// T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
|
|
488
|
-
// Persists report when observabilityDb is available; returns safe JSON subset.
|
|
489
|
-
return (async () => {
|
|
490
|
-
const adapter = createStaticUnknownAdapter();
|
|
491
|
-
const docCheckedAt = new Date().toISOString();
|
|
492
|
-
const report = probeHostCapability({
|
|
493
|
-
adapter,
|
|
494
|
-
docLinks: [],
|
|
495
|
-
docCheckedAt,
|
|
496
|
-
});
|
|
497
|
-
if (deps.observabilityDb) {
|
|
498
|
-
await recordHostCapability(deps.observabilityDb, report);
|
|
499
|
-
}
|
|
500
|
-
return {
|
|
501
|
-
ok: true,
|
|
502
|
-
command: "capability_probe",
|
|
503
|
-
data: {
|
|
504
|
-
reportId: report.reportId,
|
|
505
|
-
generatedAt: report.generatedAt,
|
|
506
|
-
deliveryTarget: report.deliveryTarget,
|
|
507
|
-
pluginLoad: { verdict: report.pluginLoad.verdict },
|
|
508
|
-
heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
|
|
509
|
-
heartbeatToolInvocation: {
|
|
510
|
-
verdict: report.heartbeatToolInvocation.verdict,
|
|
511
|
-
},
|
|
512
|
-
ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
|
|
513
|
-
conflictCount: report.conflictRecords.length,
|
|
514
|
-
recommendedNextStep: report.recommendedNextStep,
|
|
515
|
-
note: "static_local_probe: all verdicts are unknown without live host context",
|
|
516
|
-
},
|
|
517
|
-
};
|
|
518
|
-
})();
|
|
519
|
-
}
|
|
520
|
-
if (command === "near_real_smoke") {
|
|
521
|
-
// T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
|
|
522
|
-
// Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
|
|
523
|
-
if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
|
|
524
|
-
return {
|
|
525
|
-
ok: false,
|
|
526
|
-
command: "near_real_smoke",
|
|
527
|
-
error: {
|
|
528
|
-
code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
|
|
529
|
-
message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
|
|
530
|
-
nextStep: "wire_deps_into_ops_router",
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
return (async () => {
|
|
535
|
-
const result = await runNearRealConnectorSmoke({
|
|
536
|
-
state: deps.state,
|
|
537
|
-
observabilityDb: deps.observabilityDb,
|
|
538
|
-
workspaceRoot: deps.workspaceRoot,
|
|
539
|
-
});
|
|
540
|
-
return {
|
|
541
|
-
ok: true,
|
|
542
|
-
command: "near_real_smoke",
|
|
543
|
-
data: result,
|
|
544
|
-
};
|
|
545
|
-
})();
|
|
546
|
-
}
|
|
547
|
-
if (command === "connector_init") {
|
|
548
|
-
// T1.3.1 (SN-CODE-06): generate connector manifest stub.
|
|
549
|
-
return (async () => {
|
|
550
|
-
const result = await connectorInit({
|
|
551
|
-
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
552
|
-
family: typeof input?.family === "string"
|
|
553
|
-
? input.family
|
|
554
|
-
: undefined,
|
|
555
|
-
displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
|
|
556
|
-
runnerKind: typeof input?.runnerKind === "string"
|
|
557
|
-
? input.runnerKind
|
|
558
|
-
: undefined,
|
|
559
|
-
force: Boolean(input?.force),
|
|
560
|
-
workspaceRoot: deps.workspaceRoot,
|
|
561
|
-
});
|
|
562
|
-
return result;
|
|
563
|
-
})();
|
|
564
|
-
}
|
|
565
|
-
if (command === "connector_behavior_add") {
|
|
566
|
-
return connectorBehaviorAdd({
|
|
567
|
-
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
568
|
-
behaviorId: typeof input?.behaviorId === "string"
|
|
569
|
-
? input.behaviorId
|
|
570
|
-
: typeof input?.capabilityId === "string"
|
|
571
|
-
? input.capabilityId
|
|
572
|
-
: "",
|
|
573
|
-
description: typeof input?.description === "string" ? input.description : undefined,
|
|
574
|
-
channel: typeof input?.channel === "string" ? input.channel : undefined,
|
|
575
|
-
sourceRefs: input?.sourceRefs,
|
|
576
|
-
observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
|
|
577
|
-
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
578
|
-
? input.workspaceRoot
|
|
579
|
-
: deps.workspaceRoot,
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
if (command === "connector_status") {
|
|
583
|
-
return connectorStatus(deps.registry, undefined, {
|
|
584
|
-
includeHealth: Boolean(input?.includeHealth),
|
|
585
|
-
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
586
|
-
? input.workspaceRoot
|
|
587
|
-
: deps.workspaceRoot,
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
if (command === "connector_test") {
|
|
591
|
-
// v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
|
|
592
|
-
const isWet = input?.wet === true ||
|
|
593
|
-
input?.wet === "true" ||
|
|
594
|
-
input?.dryRun === false ||
|
|
595
|
-
input?.dryRun === "false";
|
|
596
|
-
const result = await connectorTest(deps.registry, {
|
|
597
|
-
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
598
|
-
dryRun: isWet ? false : (input?.dryRun === false ? false : true),
|
|
599
|
-
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
600
|
-
? input.workspaceRoot
|
|
601
|
-
: deps.workspaceRoot,
|
|
602
|
-
});
|
|
603
|
-
if (!isWet || !result.ok) {
|
|
604
|
-
return result;
|
|
605
|
-
}
|
|
606
|
-
const data = result.data && typeof result.data === "object"
|
|
607
|
-
? result.data
|
|
608
|
-
: {};
|
|
609
|
-
const capabilities = Array.isArray(data.capabilities)
|
|
610
|
-
? data.capabilities.filter((item) => typeof item === "string")
|
|
611
|
-
: [];
|
|
612
|
-
const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
|
|
613
|
-
if (!capabilityId) {
|
|
614
|
-
return {
|
|
615
|
-
ok: false,
|
|
616
|
-
command: "connector_test",
|
|
617
|
-
error: {
|
|
618
|
-
code: "MISSING_CAPABILITY_ID",
|
|
619
|
-
message: "wet connector_test requires capabilityId or at least one connector capability",
|
|
620
|
-
requiredUserInput: ["capabilityId"],
|
|
621
|
-
nextStep: "reinvoke_with_capability_id",
|
|
622
|
-
},
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
const platformId = String(data.platformId ?? input?.platformId ?? "");
|
|
626
|
-
const registryEntry = deps.registry?.describeConnector(platformId);
|
|
627
|
-
if (!registryEntry) {
|
|
628
|
-
return result;
|
|
629
|
-
}
|
|
630
|
-
const registryV7 = new CapabilityContractRegistryV7();
|
|
631
|
-
registerConnectorForWetProbe({
|
|
632
|
-
registryV7,
|
|
633
|
-
entry: {
|
|
634
|
-
platformId: registryEntry.platformId,
|
|
635
|
-
capabilities: registryEntry.capabilities,
|
|
636
|
-
manifestPath: registryEntry.manifestPath,
|
|
637
|
-
},
|
|
638
|
-
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
639
|
-
? input.workspaceRoot
|
|
640
|
-
: deps.workspaceRoot,
|
|
641
|
-
selectedCapabilityId: capabilityId,
|
|
642
|
-
safeEndpoint: textInput(input, "safeEndpoint"),
|
|
643
|
-
});
|
|
644
|
-
const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
|
|
645
|
-
const warnings = [];
|
|
646
|
-
let persistedProbeResult = false;
|
|
647
|
-
if (deps.state) {
|
|
648
|
-
await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
|
|
649
|
-
persistedProbeResult = true;
|
|
650
|
-
}
|
|
651
|
-
else {
|
|
652
|
-
warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
|
|
653
|
-
}
|
|
654
|
-
return {
|
|
655
|
-
// T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
|
|
656
|
-
// "degraded" (429/503) and "unavailable" both result in ok=false.
|
|
657
|
-
ok: wetResult.probeResult.actualStatus === "available",
|
|
658
|
-
command: "connector_test",
|
|
659
|
-
data: {
|
|
660
|
-
...data,
|
|
661
|
-
dryRun: false,
|
|
662
|
-
capabilityId,
|
|
663
|
-
actualStatus: wetResult.probeResult.actualStatus,
|
|
664
|
-
httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
|
|
665
|
-
probeResultId: wetResult.probeResult.probeResultId,
|
|
666
|
-
probeConfigRef: wetResult.probeResult.probeConfigRef,
|
|
667
|
-
sampleResponseRef: wetResult.probeResult.sampleResponseRef,
|
|
668
|
-
persistedProbeResult,
|
|
669
|
-
triggerSource: "manual_run",
|
|
670
|
-
affectsHeartbeatCadence: false,
|
|
671
|
-
note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
|
|
672
|
-
},
|
|
673
|
-
warnings,
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
if (command === "connector:run") {
|
|
677
|
-
// T-ROS.C.3: manual connector execution — isolated from heartbeat cadence
|
|
678
|
-
const platformId = typeof input?.platformId === "string" ? input.platformId : "";
|
|
679
|
-
const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
|
|
680
|
-
if (!platformId || !capabilityId) {
|
|
681
|
-
return {
|
|
682
|
-
ok: false,
|
|
683
|
-
command: "connector:run",
|
|
684
|
-
error: {
|
|
685
|
-
code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
|
|
686
|
-
message: "connector:run requires platformId and capabilityId",
|
|
687
|
-
requiredUserInput: ["platformId", "capabilityId"],
|
|
688
|
-
nextStep: "reinvoke_with_platform_and_capability_id",
|
|
689
|
-
},
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
if (!deps.connectorExecutor || !deps.state) {
|
|
693
|
-
return {
|
|
694
|
-
ok: false,
|
|
695
|
-
command: "connector:run",
|
|
696
|
-
error: {
|
|
697
|
-
code: "MANUAL_RUN_DEPS_UNAVAILABLE",
|
|
698
|
-
message: "connector:run requires connectorExecutor and state database",
|
|
699
|
-
nextStep: "wire_connector_executor_and_state_into_ops_router",
|
|
700
|
-
},
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
const toolExperienceStore = createToolExperienceStore(deps.state);
|
|
704
|
-
const experienceWriter = createExperienceWriter(toolExperienceStore);
|
|
705
|
-
const wetProbeRunner = createWetProbeRunner();
|
|
706
|
-
const registryV7 = new CapabilityContractRegistryV7();
|
|
707
|
-
// Populate V7 registry from dynamic registry if available (best-effort)
|
|
708
|
-
if (deps.registry) {
|
|
709
|
-
for (const entry of deps.registry.listConnectors()) {
|
|
710
|
-
if (entry.manifestPath) {
|
|
711
|
-
try {
|
|
712
|
-
const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
|
|
713
|
-
const manifest = JSON.parse(manifestText);
|
|
714
|
-
registryV7.register(manifest);
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
// Skip manifests that can't be read or don't validate as V7
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
const dispatcher = createManualRunDispatcher({
|
|
723
|
-
connectorExecutor: deps.connectorExecutor,
|
|
724
|
-
experienceWriter,
|
|
725
|
-
wetProbeRunner,
|
|
726
|
-
registryV7,
|
|
727
|
-
});
|
|
728
|
-
return dispatcher.runConnector({
|
|
729
|
-
platformId,
|
|
730
|
-
capabilityId,
|
|
731
|
-
payload: typeof input?.payload === "object" && input?.payload !== null
|
|
732
|
-
? input.payload
|
|
733
|
-
: undefined,
|
|
734
|
-
caller: typeof input?.caller === "string" ? input.caller : undefined,
|
|
735
|
-
reason: typeof input?.reason === "string" ? input.reason : undefined,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
738
|
-
if (command === "goal") {
|
|
739
|
-
const rawAction = typeof input?.action === "string" ? input.action : "list";
|
|
740
|
-
const action = ["set", "list", "accept", "reject"].includes(rawAction)
|
|
741
|
-
? rawAction
|
|
742
|
-
: "list";
|
|
743
|
-
const sanitizeText = (v, maxLen = 1000) => {
|
|
744
|
-
if (typeof v !== "string")
|
|
745
|
-
return undefined;
|
|
746
|
-
const trimmed = v.trim();
|
|
747
|
-
if (trimmed.length === 0)
|
|
748
|
-
return undefined;
|
|
749
|
-
return trimmed.slice(0, maxLen);
|
|
750
|
-
};
|
|
751
|
-
return goalCommand(deps.state, {
|
|
752
|
-
action,
|
|
753
|
-
goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
|
|
754
|
-
description: sanitizeText(input?.description),
|
|
755
|
-
completionCriteria: sanitizeText(input?.completionCriteria),
|
|
756
|
-
// T1.4.2: criteria alias for completionCriteria
|
|
757
|
-
criteria: sanitizeText(input?.criteria),
|
|
758
|
-
risk: typeof input?.risk === "string"
|
|
759
|
-
? input.risk
|
|
760
|
-
: undefined,
|
|
761
|
-
kind: typeof input?.kind === "string"
|
|
762
|
-
? input.kind
|
|
763
|
-
: undefined,
|
|
764
|
-
statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
|
|
765
|
-
originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
|
|
766
|
-
limit: typeof input?.limit === "number" ? input.limit : undefined,
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
if (command === "dream:recent") {
|
|
770
|
-
if (!deps.readModels) {
|
|
771
|
-
return {
|
|
772
|
-
ok: false,
|
|
773
|
-
error: {
|
|
774
|
-
code: "READ_MODELS_UNAVAILABLE",
|
|
775
|
-
message: "dream:recent requires workspace read models",
|
|
776
|
-
nextStep: "wire_read_models_into_ops_router",
|
|
777
|
-
},
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
781
|
-
const data = await deps.readModels.loadDreamRecent(limit);
|
|
782
|
-
return { ok: true, data };
|
|
783
|
-
}
|
|
784
|
-
if (command === "cycle:recent") {
|
|
785
|
-
if (!deps.readModels) {
|
|
786
|
-
return {
|
|
787
|
-
ok: false,
|
|
788
|
-
error: {
|
|
789
|
-
code: "READ_MODELS_UNAVAILABLE",
|
|
790
|
-
message: "cycle:recent requires workspace read models",
|
|
791
|
-
nextStep: "wire_read_models_into_ops_router",
|
|
792
|
-
},
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
796
|
-
const data = await deps.readModels.loadCycleRecent(limit);
|
|
797
|
-
return { ok: true, data };
|
|
798
|
-
}
|
|
799
|
-
// ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
|
|
800
|
-
/** [G2] self_health — transparent pass-through from SelfHealthSnapshot (DR-042). */
|
|
801
|
-
if (command === "self_health") {
|
|
802
|
-
const generatedAt = new Date().toISOString();
|
|
803
|
-
try {
|
|
804
|
-
ensureMinimumProbes();
|
|
805
|
-
const snap = await getSelfHealthSnapshot();
|
|
806
|
-
const degraded_dimensions = Object.entries(snap.dimensions)
|
|
807
|
-
.filter(([, d]) => d.status === "degraded")
|
|
808
|
-
.map(([k]) => k);
|
|
809
|
-
const envelope = {
|
|
810
|
-
ok: true,
|
|
811
|
-
command: "self_health",
|
|
812
|
-
runtimeMode: "workspace_full_runtime",
|
|
813
|
-
surfaceMode: "cli",
|
|
814
|
-
generatedAt,
|
|
815
|
-
data: {
|
|
816
|
-
overall: snap.overall,
|
|
817
|
-
generatedAt: snap.generatedAt,
|
|
818
|
-
degraded_dimensions,
|
|
819
|
-
dimensions: snap.dimensions,
|
|
820
|
-
},
|
|
821
|
-
warnings: [],
|
|
822
|
-
sourceRefs: ["observability/services/self-health-snapshot.ts"],
|
|
823
|
-
};
|
|
824
|
-
return envelope;
|
|
825
|
-
}
|
|
826
|
-
catch (err) {
|
|
827
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
828
|
-
const envelope = {
|
|
829
|
-
ok: false,
|
|
830
|
-
command: "self_health",
|
|
831
|
-
runtimeMode: "unavailable",
|
|
832
|
-
surfaceMode: "cli",
|
|
833
|
-
generatedAt,
|
|
834
|
-
error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
|
|
835
|
-
warnings: [],
|
|
836
|
-
sourceRefs: [],
|
|
837
|
-
};
|
|
838
|
-
return envelope;
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
/**
|
|
842
|
-
* [G3] tool_affordance — body-tool AffordanceMap pass-through.
|
|
843
|
-
* Port not yet wired in this wave; returns degraded view with clear next-step.
|
|
844
|
-
*/
|
|
845
|
-
if (command === "tool_affordance") {
|
|
846
|
-
const generatedAt = new Date().toISOString();
|
|
847
|
-
if (deps.toolAffordancePort) {
|
|
848
|
-
const allStatuses = [
|
|
849
|
-
"safe",
|
|
850
|
-
"exploratory",
|
|
851
|
-
"needs_auth",
|
|
852
|
-
"painful",
|
|
853
|
-
"unavailable",
|
|
854
|
-
];
|
|
855
|
-
const platformIds = Array.isArray(input?.platformIds)
|
|
856
|
-
? input.platformIds.filter((item) => typeof item === "string")
|
|
857
|
-
: typeof input?.platformId === "string"
|
|
858
|
-
? [input.platformId]
|
|
859
|
-
: undefined;
|
|
860
|
-
const data = await deps.toolAffordancePort.assembleAffordanceMap({
|
|
861
|
-
platformIds,
|
|
862
|
-
allowedStatuses: allStatuses,
|
|
863
|
-
goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
|
|
864
|
-
});
|
|
865
|
-
const envelope = {
|
|
866
|
-
ok: true,
|
|
867
|
-
command: "tool_affordance",
|
|
868
|
-
runtimeMode: "workspace_full_runtime",
|
|
869
|
-
surfaceMode: "cli",
|
|
870
|
-
generatedAt,
|
|
871
|
-
data,
|
|
872
|
-
warnings: [],
|
|
873
|
-
sourceRefs: [
|
|
874
|
-
"core/second-nature/body/tool-affordance/affordance-assembler.ts",
|
|
875
|
-
],
|
|
876
|
-
};
|
|
877
|
-
return envelope;
|
|
878
|
-
}
|
|
879
|
-
const envelope = {
|
|
880
|
-
ok: false,
|
|
881
|
-
command: "tool_affordance",
|
|
882
|
-
runtimeMode: "unavailable",
|
|
883
|
-
surfaceMode: "cli",
|
|
884
|
-
generatedAt,
|
|
885
|
-
error: {
|
|
886
|
-
code: "TOOL_AFFORDANCE_PORT_UNWIRED",
|
|
887
|
-
message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
|
|
888
|
-
nextStep: "wire_body_tool_port_into_ops_router_deps",
|
|
889
|
-
},
|
|
890
|
-
warnings: [],
|
|
891
|
-
sourceRefs: [],
|
|
892
|
-
};
|
|
893
|
-
return envelope;
|
|
894
|
-
}
|
|
895
|
-
/**
|
|
896
|
-
* [G6] heartbeat_digest — wraps generateHeartbeatDigest.
|
|
897
|
-
* Requires auditStore in deps; degrades if unavailable.
|
|
898
|
-
*/
|
|
899
|
-
if (command === "heartbeat_digest") {
|
|
900
|
-
const generatedAt = new Date().toISOString();
|
|
901
|
-
if (!deps.auditStore) {
|
|
902
|
-
const envelope = {
|
|
903
|
-
ok: false,
|
|
904
|
-
command: "heartbeat_digest",
|
|
905
|
-
runtimeMode: "unavailable",
|
|
906
|
-
surfaceMode: "cli",
|
|
907
|
-
generatedAt,
|
|
908
|
-
error: {
|
|
909
|
-
code: "AUDIT_STORE_UNAVAILABLE",
|
|
910
|
-
message: "heartbeat_digest requires auditStore in OpsRouterDeps",
|
|
911
|
-
nextStep: "wire_audit_store_into_ops_router",
|
|
912
|
-
},
|
|
913
|
-
warnings: [],
|
|
914
|
-
sourceRefs: [],
|
|
915
|
-
};
|
|
916
|
-
return envelope;
|
|
917
|
-
}
|
|
918
|
-
const date = typeof input?.date === "string" && input.date
|
|
919
|
-
? input.date
|
|
920
|
-
: new Date().toISOString().slice(0, 10);
|
|
921
|
-
try {
|
|
922
|
-
const digestDeps = {
|
|
923
|
-
auditStore: deps.auditStore,
|
|
924
|
-
...deps.heartbeatDigestDeps,
|
|
925
|
-
};
|
|
926
|
-
const digest = await generateHeartbeatDigest(date, digestDeps);
|
|
927
|
-
const envelope = {
|
|
928
|
-
ok: true,
|
|
929
|
-
command: "heartbeat_digest",
|
|
930
|
-
runtimeMode: "workspace_full_runtime",
|
|
931
|
-
surfaceMode: "cli",
|
|
932
|
-
generatedAt,
|
|
933
|
-
data: digest,
|
|
934
|
-
warnings: [],
|
|
935
|
-
sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
|
|
936
|
-
};
|
|
937
|
-
return envelope;
|
|
938
|
-
}
|
|
939
|
-
catch (err) {
|
|
940
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
-
const envelope = {
|
|
942
|
-
ok: false,
|
|
943
|
-
command: "heartbeat_digest",
|
|
944
|
-
runtimeMode: "unavailable",
|
|
945
|
-
surfaceMode: "cli",
|
|
946
|
-
generatedAt,
|
|
947
|
-
error: { code: "DIGEST_GENERATION_FAILED", message: msg },
|
|
948
|
-
warnings: [],
|
|
949
|
-
sourceRefs: [],
|
|
950
|
-
};
|
|
951
|
-
return envelope;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* [G6] snapshot:capture — production capture path for RestoreSnapshot +
|
|
956
|
-
* NarrativeTimeline. This gives restore and narrative:diff real state to consume.
|
|
957
|
-
*/
|
|
958
|
-
if (command === "snapshot:capture") {
|
|
959
|
-
return captureRuntimeSnapshot(deps, input);
|
|
960
|
-
}
|
|
961
|
-
/**
|
|
962
|
-
* [G6] narrative:diff — queryNarrativeDiff between two versions.
|
|
963
|
-
* Requires narrativeTimelineDeps in OpsRouterDeps.
|
|
964
|
-
*/
|
|
965
|
-
if (command === "narrative:diff") {
|
|
966
|
-
const generatedAt = new Date().toISOString();
|
|
967
|
-
if (!deps.narrativeTimelineDeps) {
|
|
968
|
-
const envelope = {
|
|
969
|
-
ok: false,
|
|
970
|
-
command: "narrative:diff",
|
|
971
|
-
runtimeMode: "unavailable",
|
|
972
|
-
surfaceMode: "cli",
|
|
973
|
-
generatedAt,
|
|
974
|
-
error: {
|
|
975
|
-
code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
|
|
976
|
-
message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
|
|
977
|
-
nextStep: "wire_narrative_timeline_deps_into_ops_router",
|
|
978
|
-
},
|
|
979
|
-
warnings: [],
|
|
980
|
-
sourceRefs: [],
|
|
981
|
-
};
|
|
982
|
-
return envelope;
|
|
983
|
-
}
|
|
984
|
-
const fromVersion = typeof input?.from === "string" ? input.from : "";
|
|
985
|
-
const toVersion = typeof input?.to === "string" ? input.to : "";
|
|
986
|
-
if (!fromVersion || !toVersion) {
|
|
987
|
-
const envelope = {
|
|
988
|
-
ok: false,
|
|
989
|
-
command: "narrative:diff",
|
|
990
|
-
runtimeMode: "workspace_full_runtime",
|
|
991
|
-
surfaceMode: "cli",
|
|
992
|
-
generatedAt,
|
|
993
|
-
error: {
|
|
994
|
-
code: "MISSING_VERSIONS",
|
|
995
|
-
message: "narrative:diff requires 'from' and 'to' version arguments",
|
|
996
|
-
nextStep: "reinvoke_with_from_and_to",
|
|
997
|
-
},
|
|
998
|
-
warnings: [],
|
|
999
|
-
sourceRefs: [],
|
|
1000
|
-
};
|
|
1001
|
-
return envelope;
|
|
1002
|
-
}
|
|
1003
|
-
try {
|
|
1004
|
-
const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
|
|
1005
|
-
const envelope = {
|
|
1006
|
-
ok: true,
|
|
1007
|
-
command: "narrative:diff",
|
|
1008
|
-
runtimeMode: "workspace_full_runtime",
|
|
1009
|
-
surfaceMode: "cli",
|
|
1010
|
-
generatedAt,
|
|
1011
|
-
data: diff,
|
|
1012
|
-
warnings: [],
|
|
1013
|
-
sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
|
|
1014
|
-
};
|
|
1015
|
-
return envelope;
|
|
1016
|
-
}
|
|
1017
|
-
catch (err) {
|
|
1018
|
-
if (err instanceof NarrativeVersionNotFoundError) {
|
|
1019
|
-
const envelope = {
|
|
1020
|
-
ok: false,
|
|
1021
|
-
command: "narrative:diff",
|
|
1022
|
-
runtimeMode: "workspace_full_runtime",
|
|
1023
|
-
surfaceMode: "cli",
|
|
1024
|
-
generatedAt,
|
|
1025
|
-
error: {
|
|
1026
|
-
code: "NARRATIVE_VERSION_NOT_FOUND",
|
|
1027
|
-
message: err.message,
|
|
1028
|
-
nextStep: "verify_version_exists_in_timeline",
|
|
1029
|
-
},
|
|
1030
|
-
warnings: [],
|
|
1031
|
-
sourceRefs: [],
|
|
1032
|
-
};
|
|
1033
|
-
return envelope;
|
|
1034
|
-
}
|
|
1035
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1036
|
-
const envelope = {
|
|
1037
|
-
ok: false,
|
|
1038
|
-
command: "narrative:diff",
|
|
1039
|
-
runtimeMode: "unavailable",
|
|
1040
|
-
surfaceMode: "cli",
|
|
1041
|
-
generatedAt,
|
|
1042
|
-
error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
|
|
1043
|
-
warnings: [],
|
|
1044
|
-
sourceRefs: [],
|
|
1045
|
-
};
|
|
1046
|
-
return envelope;
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
/**
|
|
1050
|
-
* [G6] timeline — queryNarrativeTimeline with cursor pagination.
|
|
1051
|
-
* Requires narrativeTimelineDeps in OpsRouterDeps.
|
|
1052
|
-
*/
|
|
1053
|
-
if (command === "timeline") {
|
|
1054
|
-
const generatedAt = new Date().toISOString();
|
|
1055
|
-
if (!deps.narrativeTimelineDeps) {
|
|
1056
|
-
const envelope = {
|
|
1057
|
-
ok: false,
|
|
1058
|
-
command: "timeline",
|
|
1059
|
-
runtimeMode: "unavailable",
|
|
1060
|
-
surfaceMode: "cli",
|
|
1061
|
-
generatedAt,
|
|
1062
|
-
error: {
|
|
1063
|
-
code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
|
|
1064
|
-
message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
|
|
1065
|
-
nextStep: "wire_narrative_timeline_deps_into_ops_router",
|
|
1066
|
-
},
|
|
1067
|
-
warnings: [],
|
|
1068
|
-
sourceRefs: [],
|
|
1069
|
-
};
|
|
1070
|
-
return envelope;
|
|
1071
|
-
}
|
|
1072
|
-
const now = new Date();
|
|
1073
|
-
const to = typeof input?.to === "string" ? input.to : now.toISOString();
|
|
1074
|
-
const from = typeof input?.from === "string"
|
|
1075
|
-
? input.from
|
|
1076
|
-
: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
1077
|
-
const limit = typeof input?.limit === "number" ? input.limit : 20;
|
|
1078
|
-
const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
|
|
1079
|
-
try {
|
|
1080
|
-
const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
|
|
1081
|
-
const envelope = {
|
|
1082
|
-
ok: true,
|
|
1083
|
-
command: "timeline",
|
|
1084
|
-
runtimeMode: "workspace_full_runtime",
|
|
1085
|
-
surfaceMode: "cli",
|
|
1086
|
-
generatedAt,
|
|
1087
|
-
data: page,
|
|
1088
|
-
warnings: [],
|
|
1089
|
-
sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
|
|
1090
|
-
};
|
|
1091
|
-
return envelope;
|
|
1092
|
-
}
|
|
1093
|
-
catch (err) {
|
|
1094
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1095
|
-
const code = err.name === "NarrativeQueryRangeError"
|
|
1096
|
-
? "NARRATIVE_RANGE_EXCEEDED"
|
|
1097
|
-
: "TIMELINE_QUERY_FAILED";
|
|
1098
|
-
const envelope = {
|
|
1099
|
-
ok: false,
|
|
1100
|
-
command: "timeline",
|
|
1101
|
-
runtimeMode: "unavailable",
|
|
1102
|
-
surfaceMode: "cli",
|
|
1103
|
-
generatedAt,
|
|
1104
|
-
error: { code, message: msg },
|
|
1105
|
-
warnings: [],
|
|
1106
|
-
sourceRefs: [],
|
|
1107
|
-
};
|
|
1108
|
-
return envelope;
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
/**
|
|
1112
|
-
* [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
|
|
1113
|
-
* When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
|
|
1114
|
-
* Always writes RestoreAudit. Never restores credential fields.
|
|
1115
|
-
*/
|
|
1116
|
-
if (command === "restore") {
|
|
1117
|
-
const generatedAt = new Date().toISOString();
|
|
1118
|
-
if (!deps.auditStore) {
|
|
1119
|
-
const envelope = {
|
|
1120
|
-
ok: false,
|
|
1121
|
-
command: "restore",
|
|
1122
|
-
runtimeMode: "unavailable",
|
|
1123
|
-
surfaceMode: "cli",
|
|
1124
|
-
generatedAt,
|
|
1125
|
-
error: {
|
|
1126
|
-
code: "AUDIT_STORE_UNAVAILABLE",
|
|
1127
|
-
message: "restore requires auditStore in OpsRouterDeps",
|
|
1128
|
-
nextStep: "wire_audit_store_into_ops_router",
|
|
1129
|
-
},
|
|
1130
|
-
warnings: [],
|
|
1131
|
-
sourceRefs: [],
|
|
1132
|
-
};
|
|
1133
|
-
return envelope;
|
|
1134
|
-
}
|
|
1135
|
-
let restoreTarget;
|
|
1136
|
-
let fromVersion;
|
|
1137
|
-
let toVersion;
|
|
1138
|
-
// T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
|
|
1139
|
-
// When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
|
|
1140
|
-
// matching snapshot row; otherwise fall back to explicit legacy parameters.
|
|
1141
|
-
const snapshotId = textInput(input, "snapshotId");
|
|
1142
|
-
if (snapshotId) {
|
|
1143
|
-
if (!deps.restoreSnapshotStore) {
|
|
1144
|
-
const envelope = {
|
|
1145
|
-
ok: false,
|
|
1146
|
-
command: "restore",
|
|
1147
|
-
runtimeMode: "unavailable",
|
|
1148
|
-
surfaceMode: "cli",
|
|
1149
|
-
generatedAt,
|
|
1150
|
-
error: {
|
|
1151
|
-
code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
|
|
1152
|
-
message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
|
|
1153
|
-
nextStep: "wire_restore_snapshot_store_into_ops_router",
|
|
1154
|
-
},
|
|
1155
|
-
warnings: [],
|
|
1156
|
-
sourceRefs: [],
|
|
1157
|
-
};
|
|
1158
|
-
return envelope;
|
|
1159
|
-
}
|
|
1160
|
-
const snapshots = await deps.restoreSnapshotStore.listSnapshots();
|
|
1161
|
-
const match = snapshots.find((s) => s.snapshotId === snapshotId);
|
|
1162
|
-
if (match) {
|
|
1163
|
-
restoreTarget = snapshotId;
|
|
1164
|
-
fromVersion = match.capturedAt;
|
|
1165
|
-
toVersion = snapshotId;
|
|
1166
|
-
}
|
|
1167
|
-
else {
|
|
1168
|
-
const envelope = {
|
|
1169
|
-
ok: false,
|
|
1170
|
-
command: "restore",
|
|
1171
|
-
runtimeMode: "workspace_full_runtime",
|
|
1172
|
-
surfaceMode: "cli",
|
|
1173
|
-
generatedAt,
|
|
1174
|
-
error: {
|
|
1175
|
-
code: "SNAPSHOT_NOT_FOUND",
|
|
1176
|
-
message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
|
|
1177
|
-
nextStep: "list_available_snapshots_or_verify_snapshotId",
|
|
1178
|
-
},
|
|
1179
|
-
warnings: [],
|
|
1180
|
-
sourceRefs: [],
|
|
1181
|
-
};
|
|
1182
|
-
return envelope;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
else {
|
|
1186
|
-
const missingFields = [];
|
|
1187
|
-
if (typeof input?.restoreTarget !== "string")
|
|
1188
|
-
missingFields.push("restoreTarget");
|
|
1189
|
-
if (typeof input?.fromVersion !== "string")
|
|
1190
|
-
missingFields.push("fromVersion");
|
|
1191
|
-
if (typeof input?.toVersion !== "string")
|
|
1192
|
-
missingFields.push("toVersion");
|
|
1193
|
-
if (missingFields.length > 0) {
|
|
1194
|
-
const envelope = {
|
|
1195
|
-
ok: false,
|
|
1196
|
-
command: "restore",
|
|
1197
|
-
runtimeMode: "workspace_full_runtime",
|
|
1198
|
-
surfaceMode: "cli",
|
|
1199
|
-
generatedAt,
|
|
1200
|
-
error: {
|
|
1201
|
-
code: "MISSING_RESTORE_FIELDS",
|
|
1202
|
-
message: `restore requires: ${missingFields.join(", ")}`,
|
|
1203
|
-
nextStep: "reinvoke_with_required_fields",
|
|
1204
|
-
},
|
|
1205
|
-
warnings: [],
|
|
1206
|
-
sourceRefs: [],
|
|
1207
|
-
};
|
|
1208
|
-
return envelope;
|
|
1209
|
-
}
|
|
1210
|
-
restoreTarget = input.restoreTarget;
|
|
1211
|
-
fromVersion = input.fromVersion;
|
|
1212
|
-
toVersion = input.toVersion;
|
|
1213
|
-
}
|
|
1214
|
-
// [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
|
|
1215
|
-
let restoreResult = {
|
|
1216
|
-
ok: false,
|
|
1217
|
-
completedEntities: [],
|
|
1218
|
-
failedEntities: [],
|
|
1219
|
-
warnings: ["restore_snapshot_store_unavailable"],
|
|
1220
|
-
};
|
|
1221
|
-
if (deps.restoreSnapshotStore) {
|
|
1222
|
-
restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
|
|
1223
|
-
restoreTarget: restoreTarget,
|
|
1224
|
-
fromVersion: fromVersion,
|
|
1225
|
-
toVersion: toVersion,
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
const event = {
|
|
1229
|
-
id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1230
|
-
restoreTarget: restoreTarget,
|
|
1231
|
-
fromVersion: fromVersion,
|
|
1232
|
-
toVersion: toVersion,
|
|
1233
|
-
triggeredBy: input?.triggeredBy ?? "operator",
|
|
1234
|
-
reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
|
|
1235
|
-
completedEntities: restoreResult.completedEntities,
|
|
1236
|
-
failedEntities: restoreResult.failedEntities,
|
|
1237
|
-
// credentials are always excluded from restore audit
|
|
1238
|
-
excludedFields: Array.isArray(input?.excludedFields)
|
|
1239
|
-
? input.excludedFields.filter((f) => typeof f === "string")
|
|
1240
|
-
: ["credential", "encryptionKey"],
|
|
1241
|
-
restoredFieldCount: restoreResult.completedEntities.length,
|
|
1242
|
-
createdAt: generatedAt,
|
|
1243
|
-
traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
|
|
1244
|
-
};
|
|
1245
|
-
const auditResult = await writeRestoreAudit(event, deps.auditStore);
|
|
1246
|
-
const envelope = {
|
|
1247
|
-
ok: restoreResult.ok && auditResult.ok,
|
|
1248
|
-
command: "restore",
|
|
1249
|
-
runtimeMode: "workspace_full_runtime",
|
|
1250
|
-
surfaceMode: "cli",
|
|
1251
|
-
generatedAt,
|
|
1252
|
-
data: {
|
|
1253
|
-
auditWritten: auditResult.warnings.length === 0,
|
|
1254
|
-
fromVersion: event.fromVersion,
|
|
1255
|
-
toVersion: event.toVersion,
|
|
1256
|
-
restoreTarget: event.restoreTarget,
|
|
1257
|
-
isPartialRestore: event.failedEntities.length > 0,
|
|
1258
|
-
failedEntities: event.failedEntities,
|
|
1259
|
-
completedEntities: event.completedEntities,
|
|
1260
|
-
restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
|
|
1261
|
-
},
|
|
1262
|
-
warnings: [...restoreResult.warnings, ...auditResult.warnings],
|
|
1263
|
-
sourceRefs: [
|
|
1264
|
-
"observability/services/restore-audit-service.ts",
|
|
1265
|
-
"storage/services/restore-snapshot-store.ts",
|
|
1266
|
-
],
|
|
1267
|
-
};
|
|
1268
|
-
return envelope;
|
|
1269
|
-
}
|
|
1270
|
-
/**
|
|
1271
|
-
* [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
|
|
1272
|
-
* Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
|
|
1273
|
-
*/
|
|
1274
|
-
if (command === "runtime_secret_bootstrap") {
|
|
1275
|
-
const generatedAt = new Date().toISOString();
|
|
1276
|
-
if (!deps.secretAnchorDeps) {
|
|
1277
|
-
const envelope = {
|
|
1278
|
-
ok: false,
|
|
1279
|
-
command: "runtime_secret_bootstrap",
|
|
1280
|
-
runtimeMode: "unavailable",
|
|
1281
|
-
surfaceMode: "cli",
|
|
1282
|
-
generatedAt,
|
|
1283
|
-
error: {
|
|
1284
|
-
code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
|
|
1285
|
-
message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
|
|
1286
|
-
nextStep: "wire_secret_anchor_deps_into_ops_router",
|
|
1287
|
-
},
|
|
1288
|
-
warnings: [],
|
|
1289
|
-
sourceRefs: [],
|
|
1290
|
-
};
|
|
1291
|
-
return envelope;
|
|
1292
|
-
}
|
|
1293
|
-
try {
|
|
1294
|
-
const view = await viewSecretAnchor(deps.secretAnchorDeps);
|
|
1295
|
-
// Map to RuntimeSecretBootstrapView (design model §6.1)
|
|
1296
|
-
const data = {
|
|
1297
|
-
status: view.status === "verified" || view.status === "ok"
|
|
1298
|
-
? "ok"
|
|
1299
|
-
: view.status === "missing"
|
|
1300
|
-
? "runtime_secret_anchor_missing"
|
|
1301
|
-
: view.status === "wrong_key"
|
|
1302
|
-
? "credential_recovery_required"
|
|
1303
|
-
: view.status === "decryption_failed"
|
|
1304
|
-
? "runtime_secret_unavailable"
|
|
1305
|
-
: "unknown",
|
|
1306
|
-
keyHealth: view.status === "verified" || view.status === "ok"
|
|
1307
|
-
? "ok"
|
|
1308
|
-
: view.status === "missing"
|
|
1309
|
-
? "missing_key"
|
|
1310
|
-
: view.status === "wrong_key"
|
|
1311
|
-
? "wrong_key"
|
|
1312
|
-
: "unknown",
|
|
1313
|
-
anchorLocation: view.keyPath,
|
|
1314
|
-
recoveryPrincipleRef: view.recoveryDocRef,
|
|
1315
|
-
plaintextKeyExposed: false,
|
|
1316
|
-
reasonCode: view.reasonCode,
|
|
1317
|
-
recoverySteps: view.recoverySteps,
|
|
1318
|
-
};
|
|
1319
|
-
const envelope = {
|
|
1320
|
-
ok: true,
|
|
1321
|
-
command: "runtime_secret_bootstrap",
|
|
1322
|
-
runtimeMode: "workspace_full_runtime",
|
|
1323
|
-
surfaceMode: "cli",
|
|
1324
|
-
generatedAt,
|
|
1325
|
-
data,
|
|
1326
|
-
warnings: [],
|
|
1327
|
-
sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
|
|
1328
|
-
};
|
|
1329
|
-
return envelope;
|
|
1330
|
-
}
|
|
1331
|
-
catch (err) {
|
|
1332
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1333
|
-
const envelope = {
|
|
1334
|
-
ok: false,
|
|
1335
|
-
command: "runtime_secret_bootstrap",
|
|
1336
|
-
runtimeMode: "unavailable",
|
|
1337
|
-
surfaceMode: "cli",
|
|
1338
|
-
generatedAt,
|
|
1339
|
-
error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
|
|
1340
|
-
warnings: [],
|
|
1341
|
-
sourceRefs: [],
|
|
1342
|
-
};
|
|
1343
|
-
return envelope;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
// ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
|
|
1347
|
-
// Returns the assembled impulse + atmosphere for a given scene context.
|
|
1348
|
-
// Useful for Claw to inspect what guidance content would be injected before
|
|
1349
|
-
// a real heartbeat cycle, and to verify platform-specific impulse overrides.
|
|
1350
|
-
if (command === "guidance_payload") {
|
|
1351
|
-
const generatedAt = new Date().toISOString();
|
|
1352
|
-
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1353
|
-
const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1354
|
-
const sceneType = input?.sceneType ?? "social";
|
|
1355
|
-
const capabilityIntent = typeof input?.capabilityIntent === "string"
|
|
1356
|
-
? input.capabilityIntent
|
|
1357
|
-
: undefined;
|
|
1358
|
-
const platformId = typeof input?.platformId === "string"
|
|
1359
|
-
? input.platformId
|
|
1360
|
-
: undefined;
|
|
1361
|
-
const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
|
|
1362
|
-
if (!validSceneTypes.includes(sceneType)) {
|
|
1363
|
-
const envelope = {
|
|
1364
|
-
ok: false,
|
|
1365
|
-
command: "guidance_payload",
|
|
1366
|
-
runtimeMode: "unavailable",
|
|
1367
|
-
surfaceMode: "cli",
|
|
1368
|
-
generatedAt,
|
|
1369
|
-
error: {
|
|
1370
|
-
code: "INVALID_SCENE_TYPE",
|
|
1371
|
-
message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
|
|
1372
|
-
nextStep: "reinvoke_with_valid_scene_type",
|
|
1373
|
-
},
|
|
1374
|
-
warnings: [],
|
|
1375
|
-
sourceRefs: [],
|
|
1376
|
-
};
|
|
1377
|
-
return envelope;
|
|
1378
|
-
}
|
|
1379
|
-
const impulseResult = assembleImpulseSync({
|
|
1380
|
-
sceneType: sceneType,
|
|
1381
|
-
capabilityIntent,
|
|
1382
|
-
platformId,
|
|
1383
|
-
});
|
|
1384
|
-
const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
|
|
1385
|
-
const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1386
|
-
const atmosphere = getShortAtmosphereTemplate("active", "low");
|
|
1387
|
-
const expressionBoundary = buildExpressionBoundary(sceneType);
|
|
1388
|
-
const envelope = {
|
|
1389
|
-
ok: true,
|
|
1390
|
-
command: "guidance_payload",
|
|
1391
|
-
runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
|
|
1392
|
-
surfaceMode: "cli",
|
|
1393
|
-
generatedAt,
|
|
1394
|
-
data: {
|
|
1395
|
-
sceneType,
|
|
1396
|
-
capabilityIntent: capabilityIntent ?? null,
|
|
1397
|
-
platformId: platformId ?? null,
|
|
1398
|
-
capabilityClass: impulseResult.capabilityClass,
|
|
1399
|
-
impulseSource: impulseResult.source,
|
|
1400
|
-
impulseText: impulseResult.impulse?.text ?? null,
|
|
1401
|
-
impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
|
|
1402
|
-
atmosphereText: atmosphere.text,
|
|
1403
|
-
atmosphereReviewStatus: atmosphere.reviewStatus,
|
|
1404
|
-
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1405
|
-
expressionBoundaryStyle: expressionBoundary.style,
|
|
1406
|
-
},
|
|
1407
|
-
warnings: impulseResult.source === "none"
|
|
1408
|
-
? ["no_impulse_available_for_this_scene_and_capability"]
|
|
1409
|
-
: [],
|
|
1410
|
-
sourceRefs: [
|
|
1411
|
-
"guidance/capability-class.ts",
|
|
1412
|
-
"guidance/impulse-assembler.ts",
|
|
1413
|
-
"guidance/template-registry.ts",
|
|
1414
|
-
"guidance/output-guard.ts",
|
|
1415
|
-
],
|
|
1416
|
-
};
|
|
1417
|
-
return envelope;
|
|
1418
|
-
}
|
|
1419
|
-
return {
|
|
1420
|
-
ok: false,
|
|
1421
|
-
error: {
|
|
1422
|
-
code: "unknown_ops_command",
|
|
1423
|
-
message: `Unknown ops command: ${command}`,
|
|
1424
|
-
},
|
|
1425
|
-
};
|
|
1426
|
-
},
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared ops command dispatch for CLI + tool surfaces (T1.1.3, T1.2.2).
|
|
3
|
+
*
|
|
4
|
+
* v7 additions (T-ROS.C.1): self_health, tool_affordance, connector_test --wet,
|
|
5
|
+
* heartbeat_digest, narrative:diff, timeline, restore, runtime_secret_bootstrap.
|
|
6
|
+
* All commands return RuntimeOpsEnvelope.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { heartbeatCheck, } from "./heartbeat-surface.js";
|
|
12
|
+
import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
|
|
13
|
+
import { probeHostCapability } from "../host-capability/probe-host-capability.js";
|
|
14
|
+
import { recordHostCapability } from "../host-capability/record-host-capability.js";
|
|
15
|
+
import { runNearRealConnectorSmoke } from "../../connectors/near-real/near-real-connector-smoke.js";
|
|
16
|
+
import { connectorInit } from "../commands/connector-init.js";
|
|
17
|
+
import { connectorBehaviorAdd } from "../commands/connector-behavior.js";
|
|
18
|
+
import { connectorStatus, connectorTest } from "../commands/connector-status.js";
|
|
19
|
+
import { goalCommand } from "../commands/goal.js";
|
|
20
|
+
// v7 observability services (T-ROS.C.1)
|
|
21
|
+
import { getSelfHealthSnapshot, ensureMinimumProbes, } from "../../observability/services/self-health-snapshot.js";
|
|
22
|
+
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
23
|
+
import { queryNarrativeTimeline, queryNarrativeDiff, NarrativeVersionNotFoundError, } from "../../observability/services/narrative-timeline-query-service.js";
|
|
24
|
+
import { viewSecretAnchor, } from "../../observability/services/runtime-secret-anchor-view.js";
|
|
25
|
+
import { writeRestoreAudit, } from "../../observability/services/restore-audit-service.js";
|
|
26
|
+
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
27
|
+
// T-ROS.C.3: ManualRunDispatcher and its deps
|
|
28
|
+
import { createManualRunDispatcher, } from "./manual-run-dispatcher.js";
|
|
29
|
+
import { createExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
30
|
+
import { createCapabilityProbeResultStore, createToolExperienceStore, } from "../../storage/services/tool-experience-store.js";
|
|
31
|
+
import { createWetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
|
|
32
|
+
import { CapabilityContractRegistryV7 } from "../../connectors/base/manifest-v7.js";
|
|
33
|
+
// v7 T-V7C.C.6: Dream scheduling deps for heartbeat_check quiet→dream auto-trigger
|
|
34
|
+
import { scheduleDream } from "../../dream/dream-scheduler.js";
|
|
35
|
+
import { createDreamInputLoader } from "../../dream/dream-input-loader.js";
|
|
36
|
+
import { createDiaryDreamStore } from "../../storage/services/diary-dream-store.js";
|
|
37
|
+
function coerceProbeOnlyFlag(input) {
|
|
38
|
+
const v = input?.probeOnly;
|
|
39
|
+
return v === true || v === "true" || v === 1 || v === "1";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* v7 T-V7C.C.6: Build a minimal QuietDreamSchedulePort backed by the state DB.
|
|
43
|
+
* When a source-backed Quiet write completes, this port triggers Dream scheduling
|
|
44
|
+
* via the standard scheduleDream path (rules-only mode when no model port).
|
|
45
|
+
*/
|
|
46
|
+
function createQuietDreamSchedulePort(state) {
|
|
47
|
+
return {
|
|
48
|
+
async scheduleDream({ triggerKind, runId, traceId }) {
|
|
49
|
+
const dreamStore = createDiaryDreamStore(state);
|
|
50
|
+
const inputLoader = createDreamInputLoader({ database: state });
|
|
51
|
+
const statePort = {
|
|
52
|
+
async loadDreamInputs(query) {
|
|
53
|
+
return inputLoader.loadDreamInputs(query);
|
|
54
|
+
},
|
|
55
|
+
async writeDreamOutput(output) {
|
|
56
|
+
// Bridge: dream-engine emits dream/types DreamOutput; diary-dream-store expects shared/types.
|
|
57
|
+
// Structures are identical at runtime; TS strictness requires the cast.
|
|
58
|
+
await dreamStore.appendDreamOutput(output);
|
|
59
|
+
return { outputId: output.outputId, status: "acknowledged" };
|
|
60
|
+
},
|
|
61
|
+
async markDreamOutputLifecycle(input) {
|
|
62
|
+
// transitionDreamOutputLifecycle only accepts accepted|archived.
|
|
63
|
+
if (input.newStatus !== "accepted" && input.newStatus !== "archived") {
|
|
64
|
+
return { outputId: input.outputId, status: "degraded" };
|
|
65
|
+
}
|
|
66
|
+
await dreamStore.transitionDreamOutputLifecycle(input.outputId, input.newStatus);
|
|
67
|
+
return { outputId: input.outputId, status: "acknowledged" };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
const result = await scheduleDream({
|
|
71
|
+
triggerKind,
|
|
72
|
+
runId,
|
|
73
|
+
traceId,
|
|
74
|
+
statePort,
|
|
75
|
+
windowKey: "quiet_completion",
|
|
76
|
+
});
|
|
77
|
+
return { status: result.status, reason: result.reason };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const SNAPSHOT_TABLE_BY_KIND = {
|
|
82
|
+
identity_profile: "identity_profile",
|
|
83
|
+
agent_goal: "agent_goal",
|
|
84
|
+
tool_experience: "tool_experience",
|
|
85
|
+
daily_diary: "daily_diary_index",
|
|
86
|
+
dream_output: "dream_output_index",
|
|
87
|
+
narrative_timeline: "narrative_timeline",
|
|
88
|
+
};
|
|
89
|
+
const DEFAULT_SNAPSHOT_KINDS = [
|
|
90
|
+
"identity_profile",
|
|
91
|
+
"agent_goal",
|
|
92
|
+
"tool_experience",
|
|
93
|
+
"daily_diary",
|
|
94
|
+
"dream_output",
|
|
95
|
+
"narrative_timeline",
|
|
96
|
+
];
|
|
97
|
+
function coerceRestorableKinds(value) {
|
|
98
|
+
if (!Array.isArray(value))
|
|
99
|
+
return undefined;
|
|
100
|
+
const valid = new Set(DEFAULT_SNAPSHOT_KINDS);
|
|
101
|
+
return value.filter((item) => typeof item === "string" && valid.has(item));
|
|
102
|
+
}
|
|
103
|
+
function tableExists(state, table) {
|
|
104
|
+
const result = state.sqlite.exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`, [table]);
|
|
105
|
+
return result.length > 0 && result[0].values.length > 0;
|
|
106
|
+
}
|
|
107
|
+
function readRowsFromTable(state, table) {
|
|
108
|
+
const result = state.sqlite.exec(`SELECT * FROM ${table}`);
|
|
109
|
+
if (result.length === 0 || result[0].values.length === 0)
|
|
110
|
+
return [];
|
|
111
|
+
const columns = result[0].columns;
|
|
112
|
+
return result[0].values.map((row) => {
|
|
113
|
+
const out = {};
|
|
114
|
+
columns.forEach((column, index) => {
|
|
115
|
+
out[column] = row[index];
|
|
116
|
+
});
|
|
117
|
+
return out;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function stringArray(value) {
|
|
121
|
+
return Array.isArray(value)
|
|
122
|
+
? value.filter((item) => typeof item === "string")
|
|
123
|
+
: [];
|
|
124
|
+
}
|
|
125
|
+
function textInput(input, key) {
|
|
126
|
+
const value = input?.[key];
|
|
127
|
+
if (typeof value !== "string")
|
|
128
|
+
return undefined;
|
|
129
|
+
const trimmed = value.trim();
|
|
130
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
131
|
+
}
|
|
132
|
+
function buildSnapshotNarrativeDelta(input, snapshotId, rowCounts) {
|
|
133
|
+
const explicit = input?.narrativeSnapshot &&
|
|
134
|
+
typeof input.narrativeSnapshot === "object" &&
|
|
135
|
+
!Array.isArray(input.narrativeSnapshot)
|
|
136
|
+
? input.narrativeSnapshot
|
|
137
|
+
: {};
|
|
138
|
+
const from = (key) => input?.[key] ?? explicit[key];
|
|
139
|
+
const sourceRefs = stringArray(from("sourceRefs"));
|
|
140
|
+
return {
|
|
141
|
+
focus: from("focus") ?? "workspace_state",
|
|
142
|
+
progress: from("progress") ??
|
|
143
|
+
`snapshot_captured:${Object.entries(rowCounts)
|
|
144
|
+
.map(([kind, count]) => `${kind}=${count}`)
|
|
145
|
+
.join(",")}`,
|
|
146
|
+
nextIntent: from("nextIntent") ?? "restore_ready",
|
|
147
|
+
toneSignal: from("toneSignal") ?? "system_maintenance",
|
|
148
|
+
acceptedGoalId: from("acceptedGoalId") ?? undefined,
|
|
149
|
+
sourceRefs: sourceRefs.length > 0
|
|
150
|
+
? sourceRefs
|
|
151
|
+
: [`restore_snapshot:${snapshotId}`, "runtime_ops:snapshot_capture"],
|
|
152
|
+
reasonCode: from("reasonCode") ?? "snapshot_captured",
|
|
153
|
+
summaryText: from("summaryText") ?? `Captured restore snapshot ${snapshotId}`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function hashNarrativeSnapshot(input) {
|
|
157
|
+
return createHash("sha256")
|
|
158
|
+
.update(JSON.stringify({
|
|
159
|
+
previousHash: input.previousHash,
|
|
160
|
+
snapshotId: input.snapshotId,
|
|
161
|
+
delta: input.delta,
|
|
162
|
+
createdAt: input.createdAt,
|
|
163
|
+
}))
|
|
164
|
+
.digest("hex");
|
|
165
|
+
}
|
|
166
|
+
function resolveManifestPath(manifestPath, workspaceRoot) {
|
|
167
|
+
if (path.isAbsolute(manifestPath))
|
|
168
|
+
return manifestPath;
|
|
169
|
+
return path.join(workspaceRoot ?? process.cwd(), manifestPath);
|
|
170
|
+
}
|
|
171
|
+
function registerConnectorForWetProbe(input) {
|
|
172
|
+
if (input.entry.manifestPath) {
|
|
173
|
+
try {
|
|
174
|
+
const manifestText = fs.readFileSync(resolveManifestPath(input.entry.manifestPath, input.workspaceRoot), "utf-8");
|
|
175
|
+
const parsed = JSON.parse(manifestText);
|
|
176
|
+
const registered = input.registryV7.register(parsed);
|
|
177
|
+
if (registered.ok && input.registryV7.hasCapability(input.entry.platformId, input.selectedCapabilityId)) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Non-v7 or YAML workspace manifests are projected below.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
input.registryV7.register({
|
|
186
|
+
platformId: input.entry.platformId,
|
|
187
|
+
capabilities: input.entry.capabilities.map((capabilityId) => ({
|
|
188
|
+
capabilityId,
|
|
189
|
+
intent: capabilityId,
|
|
190
|
+
probeConfig: capabilityId === input.selectedCapabilityId && input.safeEndpoint
|
|
191
|
+
? {
|
|
192
|
+
safeEndpoint: input.safeEndpoint,
|
|
193
|
+
idempotencyClass: "read_only",
|
|
194
|
+
}
|
|
195
|
+
: undefined,
|
|
196
|
+
})),
|
|
197
|
+
channelPriority: ["runtime_ops"],
|
|
198
|
+
credentialTypes: ["runtime_ops_probe"],
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function captureRuntimeSnapshot(deps, input) {
|
|
202
|
+
const generatedAt = new Date().toISOString();
|
|
203
|
+
if (!deps.state || !deps.restoreSnapshotStore) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
command: "snapshot:capture",
|
|
207
|
+
runtimeMode: "unavailable",
|
|
208
|
+
surfaceMode: "cli",
|
|
209
|
+
generatedAt,
|
|
210
|
+
error: {
|
|
211
|
+
code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
|
|
212
|
+
message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
|
|
213
|
+
nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
|
|
214
|
+
},
|
|
215
|
+
warnings: [],
|
|
216
|
+
sourceRefs: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const snapshotId = textInput(input, "snapshotId") ??
|
|
220
|
+
`snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
221
|
+
const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
|
|
222
|
+
const rowCounts = {};
|
|
223
|
+
const warnings = [];
|
|
224
|
+
for (const kind of requestedKinds) {
|
|
225
|
+
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
226
|
+
if (!tableExists(deps.state, table)) {
|
|
227
|
+
rowCounts[kind] = 0;
|
|
228
|
+
warnings.push(`table_missing:${kind}:${table}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
rowCounts[kind] = readRowsFromTable(deps.state, table).length;
|
|
232
|
+
}
|
|
233
|
+
const historyStore = createHistoryDigestStore(deps.state);
|
|
234
|
+
const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
|
|
235
|
+
const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
|
|
236
|
+
const currentHash = hashNarrativeSnapshot({
|
|
237
|
+
previousHash,
|
|
238
|
+
snapshotId,
|
|
239
|
+
delta,
|
|
240
|
+
createdAt: generatedAt,
|
|
241
|
+
});
|
|
242
|
+
await historyStore.appendNarrativeTimeline({
|
|
243
|
+
timelineId: snapshotId,
|
|
244
|
+
entryType: "owner.override",
|
|
245
|
+
subjectId: textInput(input, "subjectId") ?? snapshotId,
|
|
246
|
+
delta,
|
|
247
|
+
previousHash,
|
|
248
|
+
currentHash,
|
|
249
|
+
createdAt: generatedAt,
|
|
250
|
+
});
|
|
251
|
+
const payload = {};
|
|
252
|
+
const capturedKinds = [];
|
|
253
|
+
for (const kind of requestedKinds) {
|
|
254
|
+
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
255
|
+
if (!tableExists(deps.state, table))
|
|
256
|
+
continue;
|
|
257
|
+
const rows = readRowsFromTable(deps.state, table);
|
|
258
|
+
rowCounts[kind] = rows.length;
|
|
259
|
+
if (rows.length > 0) {
|
|
260
|
+
payload[kind] = rows;
|
|
261
|
+
capturedKinds.push(kind);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
|
|
265
|
+
snapshotId,
|
|
266
|
+
entityWhitelist: requestedKinds,
|
|
267
|
+
payload,
|
|
268
|
+
capturedAt: generatedAt,
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
command: "snapshot:capture",
|
|
273
|
+
runtimeMode: "workspace_full_runtime",
|
|
274
|
+
surfaceMode: "cli",
|
|
275
|
+
generatedAt,
|
|
276
|
+
data: {
|
|
277
|
+
snapshotId: snapshot.snapshotId,
|
|
278
|
+
capturedAt: snapshot.capturedAt,
|
|
279
|
+
entityWhitelist: snapshot.entityWhitelist,
|
|
280
|
+
capturedKinds,
|
|
281
|
+
rowCounts,
|
|
282
|
+
narrativeVersion: snapshotId,
|
|
283
|
+
},
|
|
284
|
+
warnings,
|
|
285
|
+
sourceRefs: [
|
|
286
|
+
"storage/services/restore-snapshot-store.ts",
|
|
287
|
+
"storage/services/history-digest-store.ts",
|
|
288
|
+
],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
|
|
293
|
+
* Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
|
|
294
|
+
*/
|
|
295
|
+
function createStaticUnknownAdapter() {
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
const unknownResult = (name) => ({
|
|
298
|
+
name,
|
|
299
|
+
verdict: "unknown",
|
|
300
|
+
observedAt: now,
|
|
301
|
+
reason: "static_local_probe_no_host_context",
|
|
302
|
+
evidenceRefs: [],
|
|
303
|
+
});
|
|
304
|
+
return {
|
|
305
|
+
checkPluginLoad: () => unknownResult("plugin_load"),
|
|
306
|
+
checkHeartbeatBridge: () => unknownResult("heartbeat_bridge"),
|
|
307
|
+
checkHeartbeatToolInvocation: () => unknownResult("heartbeat_tool_invocation"),
|
|
308
|
+
checkDeliveryTarget: () => ({ status: "unknown", evidenceRefs: [] }),
|
|
309
|
+
checkAckDropBehavior: () => unknownResult("ack_drop"),
|
|
310
|
+
checkHookSupport: () => [],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export function createOpsRouter(deps) {
|
|
314
|
+
return {
|
|
315
|
+
heartbeatCheck: (input) => heartbeatCheck({
|
|
316
|
+
...input,
|
|
317
|
+
runtimeAvailable: input.runtimeAvailable ?? deps.runtimeAvailable,
|
|
318
|
+
readModels: input.readModels ?? deps.readModels,
|
|
319
|
+
runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
|
|
320
|
+
state: input.state ?? deps.state,
|
|
321
|
+
workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
|
|
322
|
+
connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
|
|
323
|
+
connectorRegistry: input
|
|
324
|
+
?.connectorRegistry ?? deps.connectorRegistry,
|
|
325
|
+
digestOpts: input.digestOpts,
|
|
326
|
+
dreamSchedulePort: input.dreamSchedulePort,
|
|
327
|
+
}),
|
|
328
|
+
async dispatch(command, input) {
|
|
329
|
+
if (command === "heartbeat_check") {
|
|
330
|
+
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
331
|
+
? input.runtimeAvailable
|
|
332
|
+
: deps.runtimeAvailable;
|
|
333
|
+
// v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
|
|
334
|
+
let affordanceMap;
|
|
335
|
+
if (deps.toolAffordancePort) {
|
|
336
|
+
try {
|
|
337
|
+
affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// degrade gracefully; guard-layer will skip breaker check without affordanceMap
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
let experienceWriter;
|
|
344
|
+
if (deps.state) {
|
|
345
|
+
experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
|
|
346
|
+
}
|
|
347
|
+
// v7 T-V7C.C.6: assemble digest opts when auditStore is wired.
|
|
348
|
+
let digestOpts;
|
|
349
|
+
if (deps.auditStore) {
|
|
350
|
+
digestOpts = {
|
|
351
|
+
assemblerDeps: {
|
|
352
|
+
auditStore: deps.auditStore,
|
|
353
|
+
...deps.heartbeatDigestDeps,
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// v7 T-V7C.C.6: assemble dream schedule port when state DB is wired.
|
|
358
|
+
let dreamSchedulePort;
|
|
359
|
+
if (deps.state) {
|
|
360
|
+
dreamSchedulePort = createQuietDreamSchedulePort(deps.state);
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const result = await heartbeatCheck({
|
|
364
|
+
probeOnly: coerceProbeOnlyFlag(input),
|
|
365
|
+
runtimeAvailable,
|
|
366
|
+
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
|
|
367
|
+
typeof input.fakeControlPlanePassthrough === "object"
|
|
368
|
+
? input.fakeControlPlanePassthrough
|
|
369
|
+
: undefined,
|
|
370
|
+
readModels: input?.readModels ??
|
|
371
|
+
deps.readModels,
|
|
372
|
+
runtimeRecorder: input
|
|
373
|
+
?.runtimeRecorder ?? deps.runtimeRecorder,
|
|
374
|
+
state: input?.state ??
|
|
375
|
+
deps.state,
|
|
376
|
+
workspaceRoot: input
|
|
377
|
+
?.workspaceRoot ?? deps.workspaceRoot,
|
|
378
|
+
timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
|
|
379
|
+
sessionContext: typeof input?.sessionContext === "string"
|
|
380
|
+
? input.sessionContext
|
|
381
|
+
: undefined,
|
|
382
|
+
scopeHint: input?.scopeHint,
|
|
383
|
+
connectorExecutor: input
|
|
384
|
+
?.connectorExecutor ?? deps.connectorExecutor,
|
|
385
|
+
connectorRegistry: input
|
|
386
|
+
?.connectorRegistry ?? deps.connectorRegistry,
|
|
387
|
+
affordanceMap,
|
|
388
|
+
experienceWriter,
|
|
389
|
+
digestOpts,
|
|
390
|
+
dreamSchedulePort,
|
|
391
|
+
});
|
|
392
|
+
if (result.ok &&
|
|
393
|
+
result.surfaceMode === "workspace_full_runtime" &&
|
|
394
|
+
!coerceProbeOnlyFlag(input) &&
|
|
395
|
+
deps.state &&
|
|
396
|
+
deps.restoreSnapshotStore) {
|
|
397
|
+
try {
|
|
398
|
+
const capture = await captureRuntimeSnapshot(deps, {
|
|
399
|
+
snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
|
|
400
|
+
subjectId: result.decisionId ?? "heartbeat_check",
|
|
401
|
+
reasonCode: "heartbeat_check",
|
|
402
|
+
summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
|
|
403
|
+
focus: result.status,
|
|
404
|
+
progress: result.reasons.join(",") || "heartbeat_completed",
|
|
405
|
+
nextIntent: "continue_runtime_loop",
|
|
406
|
+
sourceRefs: result.decisionId
|
|
407
|
+
? [`heartbeat:${result.decisionId}`]
|
|
408
|
+
: ["heartbeat:runtime"],
|
|
409
|
+
});
|
|
410
|
+
if (capture.ok) {
|
|
411
|
+
result.reasons = [...result.reasons, "restore_snapshot_captured"];
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
416
|
+
result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
+
const envelope = {
|
|
424
|
+
ok: false,
|
|
425
|
+
command: "heartbeat_check",
|
|
426
|
+
runtimeMode: runtimeAvailable ? "workspace_full_runtime" : "unavailable",
|
|
427
|
+
surfaceMode: "cli",
|
|
428
|
+
generatedAt: new Date().toISOString(),
|
|
429
|
+
error: {
|
|
430
|
+
code: "HEARTBEAT_CYCLE_EXCEPTION",
|
|
431
|
+
message: `heartbeat_check cycle threw unexpectedly: ${msg.slice(0, 200)}`,
|
|
432
|
+
nextStep: "check_logs_and_report",
|
|
433
|
+
},
|
|
434
|
+
warnings: [],
|
|
435
|
+
sourceRefs: [],
|
|
436
|
+
};
|
|
437
|
+
return envelope;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (command === "fallback") {
|
|
441
|
+
const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
|
|
442
|
+
if (!ref) {
|
|
443
|
+
return {
|
|
444
|
+
ok: false,
|
|
445
|
+
error: {
|
|
446
|
+
code: "MISSING_FALLBACK_REF",
|
|
447
|
+
message: "fallback requires args.ref (e.g. fallback:…)",
|
|
448
|
+
requiredUserInput: ["ref"],
|
|
449
|
+
nextStep: "reinvoke_with_ref",
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
if (!deps.readModels?.loadFallbackView) {
|
|
454
|
+
return {
|
|
455
|
+
ok: false,
|
|
456
|
+
error: {
|
|
457
|
+
code: "FALLBACK_READ_MODEL_UNAVAILABLE",
|
|
458
|
+
message: "Operator fallback view requires workspace read models",
|
|
459
|
+
requiredUserInput: ["ref"],
|
|
460
|
+
nextStep: "wire_read_models_into_ops_router",
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return (async () => {
|
|
465
|
+
try {
|
|
466
|
+
const data = await showOperatorFallback(ref, deps.readModels);
|
|
467
|
+
return { ok: true, command: "fallback", data };
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
if (error instanceof OperatorFallbackNotFoundError) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false,
|
|
473
|
+
command: "fallback",
|
|
474
|
+
error: {
|
|
475
|
+
code: error.code,
|
|
476
|
+
message: error.message,
|
|
477
|
+
requiredUserInput: ["ref"],
|
|
478
|
+
nextStep: "verify_fallback_ref_from_delivery_audit",
|
|
479
|
+
},
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
})();
|
|
485
|
+
}
|
|
486
|
+
if (command === "capability_probe") {
|
|
487
|
+
// T1.2.8 (SN-CODE-03): run host capability probe with static unknown adapter (CLI context).
|
|
488
|
+
// Persists report when observabilityDb is available; returns safe JSON subset.
|
|
489
|
+
return (async () => {
|
|
490
|
+
const adapter = createStaticUnknownAdapter();
|
|
491
|
+
const docCheckedAt = new Date().toISOString();
|
|
492
|
+
const report = probeHostCapability({
|
|
493
|
+
adapter,
|
|
494
|
+
docLinks: [],
|
|
495
|
+
docCheckedAt,
|
|
496
|
+
});
|
|
497
|
+
if (deps.observabilityDb) {
|
|
498
|
+
await recordHostCapability(deps.observabilityDb, report);
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
ok: true,
|
|
502
|
+
command: "capability_probe",
|
|
503
|
+
data: {
|
|
504
|
+
reportId: report.reportId,
|
|
505
|
+
generatedAt: report.generatedAt,
|
|
506
|
+
deliveryTarget: report.deliveryTarget,
|
|
507
|
+
pluginLoad: { verdict: report.pluginLoad.verdict },
|
|
508
|
+
heartbeatBridge: { verdict: report.heartbeatBridge.verdict },
|
|
509
|
+
heartbeatToolInvocation: {
|
|
510
|
+
verdict: report.heartbeatToolInvocation.verdict,
|
|
511
|
+
},
|
|
512
|
+
ackDropBehavior: { verdict: report.ackDropBehavior.verdict },
|
|
513
|
+
conflictCount: report.conflictRecords.length,
|
|
514
|
+
recommendedNextStep: report.recommendedNextStep,
|
|
515
|
+
note: "static_local_probe: all verdicts are unknown without live host context",
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
})();
|
|
519
|
+
}
|
|
520
|
+
if (command === "near_real_smoke") {
|
|
521
|
+
// T3.3.2 (SN-CODE-05): wrap runNearRealConnectorSmoke as an ops surface command.
|
|
522
|
+
// Requires state + observabilityDb + workspaceRoot to be wired into OpsRouterDeps.
|
|
523
|
+
if (!deps.state || !deps.observabilityDb || !deps.workspaceRoot) {
|
|
524
|
+
return {
|
|
525
|
+
ok: false,
|
|
526
|
+
command: "near_real_smoke",
|
|
527
|
+
error: {
|
|
528
|
+
code: "NEAR_REAL_SMOKE_DEPS_UNAVAILABLE",
|
|
529
|
+
message: "near_real_smoke requires state, observabilityDb, and workspaceRoot in OpsRouterDeps",
|
|
530
|
+
nextStep: "wire_deps_into_ops_router",
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return (async () => {
|
|
535
|
+
const result = await runNearRealConnectorSmoke({
|
|
536
|
+
state: deps.state,
|
|
537
|
+
observabilityDb: deps.observabilityDb,
|
|
538
|
+
workspaceRoot: deps.workspaceRoot,
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
ok: true,
|
|
542
|
+
command: "near_real_smoke",
|
|
543
|
+
data: result,
|
|
544
|
+
};
|
|
545
|
+
})();
|
|
546
|
+
}
|
|
547
|
+
if (command === "connector_init") {
|
|
548
|
+
// T1.3.1 (SN-CODE-06): generate connector manifest stub.
|
|
549
|
+
return (async () => {
|
|
550
|
+
const result = await connectorInit({
|
|
551
|
+
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
552
|
+
family: typeof input?.family === "string"
|
|
553
|
+
? input.family
|
|
554
|
+
: undefined,
|
|
555
|
+
displayName: typeof input?.displayName === "string" ? input.displayName : undefined,
|
|
556
|
+
runnerKind: typeof input?.runnerKind === "string"
|
|
557
|
+
? input.runnerKind
|
|
558
|
+
: undefined,
|
|
559
|
+
force: Boolean(input?.force),
|
|
560
|
+
workspaceRoot: deps.workspaceRoot,
|
|
561
|
+
});
|
|
562
|
+
return result;
|
|
563
|
+
})();
|
|
564
|
+
}
|
|
565
|
+
if (command === "connector_behavior_add") {
|
|
566
|
+
return connectorBehaviorAdd({
|
|
567
|
+
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
568
|
+
behaviorId: typeof input?.behaviorId === "string"
|
|
569
|
+
? input.behaviorId
|
|
570
|
+
: typeof input?.capabilityId === "string"
|
|
571
|
+
? input.capabilityId
|
|
572
|
+
: "",
|
|
573
|
+
description: typeof input?.description === "string" ? input.description : undefined,
|
|
574
|
+
channel: typeof input?.channel === "string" ? input.channel : undefined,
|
|
575
|
+
sourceRefs: input?.sourceRefs,
|
|
576
|
+
observedCount: typeof input?.observedCount === "number" ? input.observedCount : undefined,
|
|
577
|
+
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
578
|
+
? input.workspaceRoot
|
|
579
|
+
: deps.workspaceRoot,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (command === "connector_status") {
|
|
583
|
+
return connectorStatus(deps.registry, undefined, {
|
|
584
|
+
includeHealth: Boolean(input?.includeHealth),
|
|
585
|
+
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
586
|
+
? input.workspaceRoot
|
|
587
|
+
: deps.workspaceRoot,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (command === "connector_test") {
|
|
591
|
+
// v7 T-V7C.C.1: dryRun=false is the canonical wet probe switch.
|
|
592
|
+
const isWet = input?.wet === true ||
|
|
593
|
+
input?.wet === "true" ||
|
|
594
|
+
input?.dryRun === false ||
|
|
595
|
+
input?.dryRun === "false";
|
|
596
|
+
const result = await connectorTest(deps.registry, {
|
|
597
|
+
platformId: typeof input?.platformId === "string" ? input.platformId : "",
|
|
598
|
+
dryRun: isWet ? false : (input?.dryRun === false ? false : true),
|
|
599
|
+
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
600
|
+
? input.workspaceRoot
|
|
601
|
+
: deps.workspaceRoot,
|
|
602
|
+
});
|
|
603
|
+
if (!isWet || !result.ok) {
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
const data = result.data && typeof result.data === "object"
|
|
607
|
+
? result.data
|
|
608
|
+
: {};
|
|
609
|
+
const capabilities = Array.isArray(data.capabilities)
|
|
610
|
+
? data.capabilities.filter((item) => typeof item === "string")
|
|
611
|
+
: [];
|
|
612
|
+
const capabilityId = textInput(input, "capabilityId") ?? capabilities[0] ?? "";
|
|
613
|
+
if (!capabilityId) {
|
|
614
|
+
return {
|
|
615
|
+
ok: false,
|
|
616
|
+
command: "connector_test",
|
|
617
|
+
error: {
|
|
618
|
+
code: "MISSING_CAPABILITY_ID",
|
|
619
|
+
message: "wet connector_test requires capabilityId or at least one connector capability",
|
|
620
|
+
requiredUserInput: ["capabilityId"],
|
|
621
|
+
nextStep: "reinvoke_with_capability_id",
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const platformId = String(data.platformId ?? input?.platformId ?? "");
|
|
626
|
+
const registryEntry = deps.registry?.describeConnector(platformId);
|
|
627
|
+
if (!registryEntry) {
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
const registryV7 = new CapabilityContractRegistryV7();
|
|
631
|
+
registerConnectorForWetProbe({
|
|
632
|
+
registryV7,
|
|
633
|
+
entry: {
|
|
634
|
+
platformId: registryEntry.platformId,
|
|
635
|
+
capabilities: registryEntry.capabilities,
|
|
636
|
+
manifestPath: registryEntry.manifestPath,
|
|
637
|
+
},
|
|
638
|
+
workspaceRoot: typeof input?.workspaceRoot === "string"
|
|
639
|
+
? input.workspaceRoot
|
|
640
|
+
: deps.workspaceRoot,
|
|
641
|
+
selectedCapabilityId: capabilityId,
|
|
642
|
+
safeEndpoint: textInput(input, "safeEndpoint"),
|
|
643
|
+
});
|
|
644
|
+
const wetResult = await createWetProbeRunner().runWetProbe(platformId, capabilityId, registryV7);
|
|
645
|
+
const warnings = [];
|
|
646
|
+
let persistedProbeResult = false;
|
|
647
|
+
if (deps.state) {
|
|
648
|
+
await createCapabilityProbeResultStore(deps.state).appendProbeResult(wetResult.probeResult);
|
|
649
|
+
persistedProbeResult = true;
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
// T-V7C.C.5: only "available" (HTTP 200-299) counts as success;
|
|
656
|
+
// "degraded" (429/503) and "unavailable" both result in ok=false.
|
|
657
|
+
ok: wetResult.probeResult.actualStatus === "available",
|
|
658
|
+
command: "connector_test",
|
|
659
|
+
data: {
|
|
660
|
+
...data,
|
|
661
|
+
dryRun: false,
|
|
662
|
+
capabilityId,
|
|
663
|
+
actualStatus: wetResult.probeResult.actualStatus,
|
|
664
|
+
httpStatus: wetResult.probeResult.httpStatus ?? wetResult.httpStatus,
|
|
665
|
+
probeResultId: wetResult.probeResult.probeResultId,
|
|
666
|
+
probeConfigRef: wetResult.probeResult.probeConfigRef,
|
|
667
|
+
sampleResponseRef: wetResult.probeResult.sampleResponseRef,
|
|
668
|
+
persistedProbeResult,
|
|
669
|
+
triggerSource: "manual_run",
|
|
670
|
+
affectsHeartbeatCadence: false,
|
|
671
|
+
note: "wet probe mode: executed safe probe endpoint and persisted capability_probe_result when state DB is available",
|
|
672
|
+
},
|
|
673
|
+
warnings,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (command === "connector:run") {
|
|
677
|
+
// T-ROS.C.3: manual connector execution — isolated from heartbeat cadence
|
|
678
|
+
const platformId = typeof input?.platformId === "string" ? input.platformId : "";
|
|
679
|
+
const capabilityId = typeof input?.capabilityId === "string" ? input.capabilityId : "";
|
|
680
|
+
if (!platformId || !capabilityId) {
|
|
681
|
+
return {
|
|
682
|
+
ok: false,
|
|
683
|
+
command: "connector:run",
|
|
684
|
+
error: {
|
|
685
|
+
code: "MISSING_PLATFORM_OR_CAPABILITY_ID",
|
|
686
|
+
message: "connector:run requires platformId and capabilityId",
|
|
687
|
+
requiredUserInput: ["platformId", "capabilityId"],
|
|
688
|
+
nextStep: "reinvoke_with_platform_and_capability_id",
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (!deps.connectorExecutor || !deps.state) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
command: "connector:run",
|
|
696
|
+
error: {
|
|
697
|
+
code: "MANUAL_RUN_DEPS_UNAVAILABLE",
|
|
698
|
+
message: "connector:run requires connectorExecutor and state database",
|
|
699
|
+
nextStep: "wire_connector_executor_and_state_into_ops_router",
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const toolExperienceStore = createToolExperienceStore(deps.state);
|
|
704
|
+
const experienceWriter = createExperienceWriter(toolExperienceStore);
|
|
705
|
+
const wetProbeRunner = createWetProbeRunner();
|
|
706
|
+
const registryV7 = new CapabilityContractRegistryV7();
|
|
707
|
+
// Populate V7 registry from dynamic registry if available (best-effort)
|
|
708
|
+
if (deps.registry) {
|
|
709
|
+
for (const entry of deps.registry.listConnectors()) {
|
|
710
|
+
if (entry.manifestPath) {
|
|
711
|
+
try {
|
|
712
|
+
const manifestText = fs.readFileSync(entry.manifestPath, "utf-8");
|
|
713
|
+
const manifest = JSON.parse(manifestText);
|
|
714
|
+
registryV7.register(manifest);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
// Skip manifests that can't be read or don't validate as V7
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const dispatcher = createManualRunDispatcher({
|
|
723
|
+
connectorExecutor: deps.connectorExecutor,
|
|
724
|
+
experienceWriter,
|
|
725
|
+
wetProbeRunner,
|
|
726
|
+
registryV7,
|
|
727
|
+
});
|
|
728
|
+
return dispatcher.runConnector({
|
|
729
|
+
platformId,
|
|
730
|
+
capabilityId,
|
|
731
|
+
payload: typeof input?.payload === "object" && input?.payload !== null
|
|
732
|
+
? input.payload
|
|
733
|
+
: undefined,
|
|
734
|
+
caller: typeof input?.caller === "string" ? input.caller : undefined,
|
|
735
|
+
reason: typeof input?.reason === "string" ? input.reason : undefined,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (command === "goal") {
|
|
739
|
+
const rawAction = typeof input?.action === "string" ? input.action : "list";
|
|
740
|
+
const action = ["set", "list", "accept", "reject"].includes(rawAction)
|
|
741
|
+
? rawAction
|
|
742
|
+
: "list";
|
|
743
|
+
const sanitizeText = (v, maxLen = 1000) => {
|
|
744
|
+
if (typeof v !== "string")
|
|
745
|
+
return undefined;
|
|
746
|
+
const trimmed = v.trim();
|
|
747
|
+
if (trimmed.length === 0)
|
|
748
|
+
return undefined;
|
|
749
|
+
return trimmed.slice(0, maxLen);
|
|
750
|
+
};
|
|
751
|
+
return goalCommand(deps.state, {
|
|
752
|
+
action,
|
|
753
|
+
goalId: typeof input?.goalId === "string" ? input.goalId.trim().slice(0, 128) : undefined,
|
|
754
|
+
description: sanitizeText(input?.description),
|
|
755
|
+
completionCriteria: sanitizeText(input?.completionCriteria),
|
|
756
|
+
// T1.4.2: criteria alias for completionCriteria
|
|
757
|
+
criteria: sanitizeText(input?.criteria),
|
|
758
|
+
risk: typeof input?.risk === "string"
|
|
759
|
+
? input.risk
|
|
760
|
+
: undefined,
|
|
761
|
+
kind: typeof input?.kind === "string"
|
|
762
|
+
? input.kind
|
|
763
|
+
: undefined,
|
|
764
|
+
statusFilter: typeof input?.statusFilter === "string" ? input.statusFilter : undefined,
|
|
765
|
+
originFilter: typeof input?.originFilter === "string" ? input.originFilter : undefined,
|
|
766
|
+
limit: typeof input?.limit === "number" ? input.limit : undefined,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (command === "dream:recent") {
|
|
770
|
+
if (!deps.readModels) {
|
|
771
|
+
return {
|
|
772
|
+
ok: false,
|
|
773
|
+
error: {
|
|
774
|
+
code: "READ_MODELS_UNAVAILABLE",
|
|
775
|
+
message: "dream:recent requires workspace read models",
|
|
776
|
+
nextStep: "wire_read_models_into_ops_router",
|
|
777
|
+
},
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
781
|
+
const data = await deps.readModels.loadDreamRecent(limit);
|
|
782
|
+
return { ok: true, data };
|
|
783
|
+
}
|
|
784
|
+
if (command === "cycle:recent") {
|
|
785
|
+
if (!deps.readModels) {
|
|
786
|
+
return {
|
|
787
|
+
ok: false,
|
|
788
|
+
error: {
|
|
789
|
+
code: "READ_MODELS_UNAVAILABLE",
|
|
790
|
+
message: "cycle:recent requires workspace read models",
|
|
791
|
+
nextStep: "wire_read_models_into_ops_router",
|
|
792
|
+
},
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
const limit = typeof input?.limit === "number" ? input.limit : 5;
|
|
796
|
+
const data = await deps.readModels.loadCycleRecent(limit);
|
|
797
|
+
return { ok: true, data };
|
|
798
|
+
}
|
|
799
|
+
// ─── v7 commands (T-ROS.C.1) ─────────────────────────────────────────
|
|
800
|
+
/** [G2] self_health — transparent pass-through from SelfHealthSnapshot (DR-042). */
|
|
801
|
+
if (command === "self_health") {
|
|
802
|
+
const generatedAt = new Date().toISOString();
|
|
803
|
+
try {
|
|
804
|
+
ensureMinimumProbes();
|
|
805
|
+
const snap = await getSelfHealthSnapshot();
|
|
806
|
+
const degraded_dimensions = Object.entries(snap.dimensions)
|
|
807
|
+
.filter(([, d]) => d.status === "degraded")
|
|
808
|
+
.map(([k]) => k);
|
|
809
|
+
const envelope = {
|
|
810
|
+
ok: true,
|
|
811
|
+
command: "self_health",
|
|
812
|
+
runtimeMode: "workspace_full_runtime",
|
|
813
|
+
surfaceMode: "cli",
|
|
814
|
+
generatedAt,
|
|
815
|
+
data: {
|
|
816
|
+
overall: snap.overall,
|
|
817
|
+
generatedAt: snap.generatedAt,
|
|
818
|
+
degraded_dimensions,
|
|
819
|
+
dimensions: snap.dimensions,
|
|
820
|
+
},
|
|
821
|
+
warnings: [],
|
|
822
|
+
sourceRefs: ["observability/services/self-health-snapshot.ts"],
|
|
823
|
+
};
|
|
824
|
+
return envelope;
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
828
|
+
const envelope = {
|
|
829
|
+
ok: false,
|
|
830
|
+
command: "self_health",
|
|
831
|
+
runtimeMode: "unavailable",
|
|
832
|
+
surfaceMode: "cli",
|
|
833
|
+
generatedAt,
|
|
834
|
+
error: { code: "SELF_HEALTH_PROBE_FAILED", message: msg },
|
|
835
|
+
warnings: [],
|
|
836
|
+
sourceRefs: [],
|
|
837
|
+
};
|
|
838
|
+
return envelope;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* [G3] tool_affordance — body-tool AffordanceMap pass-through.
|
|
843
|
+
* Port not yet wired in this wave; returns degraded view with clear next-step.
|
|
844
|
+
*/
|
|
845
|
+
if (command === "tool_affordance") {
|
|
846
|
+
const generatedAt = new Date().toISOString();
|
|
847
|
+
if (deps.toolAffordancePort) {
|
|
848
|
+
const allStatuses = [
|
|
849
|
+
"safe",
|
|
850
|
+
"exploratory",
|
|
851
|
+
"needs_auth",
|
|
852
|
+
"painful",
|
|
853
|
+
"unavailable",
|
|
854
|
+
];
|
|
855
|
+
const platformIds = Array.isArray(input?.platformIds)
|
|
856
|
+
? input.platformIds.filter((item) => typeof item === "string")
|
|
857
|
+
: typeof input?.platformId === "string"
|
|
858
|
+
? [input.platformId]
|
|
859
|
+
: undefined;
|
|
860
|
+
const data = await deps.toolAffordancePort.assembleAffordanceMap({
|
|
861
|
+
platformIds,
|
|
862
|
+
allowedStatuses: allStatuses,
|
|
863
|
+
goalKind: typeof input?.goalKind === "string" ? input.goalKind : undefined,
|
|
864
|
+
});
|
|
865
|
+
const envelope = {
|
|
866
|
+
ok: true,
|
|
867
|
+
command: "tool_affordance",
|
|
868
|
+
runtimeMode: "workspace_full_runtime",
|
|
869
|
+
surfaceMode: "cli",
|
|
870
|
+
generatedAt,
|
|
871
|
+
data,
|
|
872
|
+
warnings: [],
|
|
873
|
+
sourceRefs: [
|
|
874
|
+
"core/second-nature/body/tool-affordance/affordance-assembler.ts",
|
|
875
|
+
],
|
|
876
|
+
};
|
|
877
|
+
return envelope;
|
|
878
|
+
}
|
|
879
|
+
const envelope = {
|
|
880
|
+
ok: false,
|
|
881
|
+
command: "tool_affordance",
|
|
882
|
+
runtimeMode: "unavailable",
|
|
883
|
+
surfaceMode: "cli",
|
|
884
|
+
generatedAt,
|
|
885
|
+
error: {
|
|
886
|
+
code: "TOOL_AFFORDANCE_PORT_UNWIRED",
|
|
887
|
+
message: "tool_affordance requires body-tool AffordanceMap port (T-BTS.C.1) to be wired into OpsRouterDeps",
|
|
888
|
+
nextStep: "wire_body_tool_port_into_ops_router_deps",
|
|
889
|
+
},
|
|
890
|
+
warnings: [],
|
|
891
|
+
sourceRefs: [],
|
|
892
|
+
};
|
|
893
|
+
return envelope;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* [G6] heartbeat_digest — wraps generateHeartbeatDigest.
|
|
897
|
+
* Requires auditStore in deps; degrades if unavailable.
|
|
898
|
+
*/
|
|
899
|
+
if (command === "heartbeat_digest") {
|
|
900
|
+
const generatedAt = new Date().toISOString();
|
|
901
|
+
if (!deps.auditStore) {
|
|
902
|
+
const envelope = {
|
|
903
|
+
ok: false,
|
|
904
|
+
command: "heartbeat_digest",
|
|
905
|
+
runtimeMode: "unavailable",
|
|
906
|
+
surfaceMode: "cli",
|
|
907
|
+
generatedAt,
|
|
908
|
+
error: {
|
|
909
|
+
code: "AUDIT_STORE_UNAVAILABLE",
|
|
910
|
+
message: "heartbeat_digest requires auditStore in OpsRouterDeps",
|
|
911
|
+
nextStep: "wire_audit_store_into_ops_router",
|
|
912
|
+
},
|
|
913
|
+
warnings: [],
|
|
914
|
+
sourceRefs: [],
|
|
915
|
+
};
|
|
916
|
+
return envelope;
|
|
917
|
+
}
|
|
918
|
+
const date = typeof input?.date === "string" && input.date
|
|
919
|
+
? input.date
|
|
920
|
+
: new Date().toISOString().slice(0, 10);
|
|
921
|
+
try {
|
|
922
|
+
const digestDeps = {
|
|
923
|
+
auditStore: deps.auditStore,
|
|
924
|
+
...deps.heartbeatDigestDeps,
|
|
925
|
+
};
|
|
926
|
+
const digest = await generateHeartbeatDigest(date, digestDeps);
|
|
927
|
+
const envelope = {
|
|
928
|
+
ok: true,
|
|
929
|
+
command: "heartbeat_digest",
|
|
930
|
+
runtimeMode: "workspace_full_runtime",
|
|
931
|
+
surfaceMode: "cli",
|
|
932
|
+
generatedAt,
|
|
933
|
+
data: digest,
|
|
934
|
+
warnings: [],
|
|
935
|
+
sourceRefs: ["observability/services/heartbeat-digest-assembler.ts"],
|
|
936
|
+
};
|
|
937
|
+
return envelope;
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
const envelope = {
|
|
942
|
+
ok: false,
|
|
943
|
+
command: "heartbeat_digest",
|
|
944
|
+
runtimeMode: "unavailable",
|
|
945
|
+
surfaceMode: "cli",
|
|
946
|
+
generatedAt,
|
|
947
|
+
error: { code: "DIGEST_GENERATION_FAILED", message: msg },
|
|
948
|
+
warnings: [],
|
|
949
|
+
sourceRefs: [],
|
|
950
|
+
};
|
|
951
|
+
return envelope;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* [G6] snapshot:capture — production capture path for RestoreSnapshot +
|
|
956
|
+
* NarrativeTimeline. This gives restore and narrative:diff real state to consume.
|
|
957
|
+
*/
|
|
958
|
+
if (command === "snapshot:capture") {
|
|
959
|
+
return captureRuntimeSnapshot(deps, input);
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* [G6] narrative:diff — queryNarrativeDiff between two versions.
|
|
963
|
+
* Requires narrativeTimelineDeps in OpsRouterDeps.
|
|
964
|
+
*/
|
|
965
|
+
if (command === "narrative:diff") {
|
|
966
|
+
const generatedAt = new Date().toISOString();
|
|
967
|
+
if (!deps.narrativeTimelineDeps) {
|
|
968
|
+
const envelope = {
|
|
969
|
+
ok: false,
|
|
970
|
+
command: "narrative:diff",
|
|
971
|
+
runtimeMode: "unavailable",
|
|
972
|
+
surfaceMode: "cli",
|
|
973
|
+
generatedAt,
|
|
974
|
+
error: {
|
|
975
|
+
code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
|
|
976
|
+
message: "narrative:diff requires narrativeTimelineDeps in OpsRouterDeps",
|
|
977
|
+
nextStep: "wire_narrative_timeline_deps_into_ops_router",
|
|
978
|
+
},
|
|
979
|
+
warnings: [],
|
|
980
|
+
sourceRefs: [],
|
|
981
|
+
};
|
|
982
|
+
return envelope;
|
|
983
|
+
}
|
|
984
|
+
const fromVersion = typeof input?.from === "string" ? input.from : "";
|
|
985
|
+
const toVersion = typeof input?.to === "string" ? input.to : "";
|
|
986
|
+
if (!fromVersion || !toVersion) {
|
|
987
|
+
const envelope = {
|
|
988
|
+
ok: false,
|
|
989
|
+
command: "narrative:diff",
|
|
990
|
+
runtimeMode: "workspace_full_runtime",
|
|
991
|
+
surfaceMode: "cli",
|
|
992
|
+
generatedAt,
|
|
993
|
+
error: {
|
|
994
|
+
code: "MISSING_VERSIONS",
|
|
995
|
+
message: "narrative:diff requires 'from' and 'to' version arguments",
|
|
996
|
+
nextStep: "reinvoke_with_from_and_to",
|
|
997
|
+
},
|
|
998
|
+
warnings: [],
|
|
999
|
+
sourceRefs: [],
|
|
1000
|
+
};
|
|
1001
|
+
return envelope;
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const diff = await queryNarrativeDiff(fromVersion, toVersion, deps.narrativeTimelineDeps);
|
|
1005
|
+
const envelope = {
|
|
1006
|
+
ok: true,
|
|
1007
|
+
command: "narrative:diff",
|
|
1008
|
+
runtimeMode: "workspace_full_runtime",
|
|
1009
|
+
surfaceMode: "cli",
|
|
1010
|
+
generatedAt,
|
|
1011
|
+
data: diff,
|
|
1012
|
+
warnings: [],
|
|
1013
|
+
sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
|
|
1014
|
+
};
|
|
1015
|
+
return envelope;
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
if (err instanceof NarrativeVersionNotFoundError) {
|
|
1019
|
+
const envelope = {
|
|
1020
|
+
ok: false,
|
|
1021
|
+
command: "narrative:diff",
|
|
1022
|
+
runtimeMode: "workspace_full_runtime",
|
|
1023
|
+
surfaceMode: "cli",
|
|
1024
|
+
generatedAt,
|
|
1025
|
+
error: {
|
|
1026
|
+
code: "NARRATIVE_VERSION_NOT_FOUND",
|
|
1027
|
+
message: err.message,
|
|
1028
|
+
nextStep: "verify_version_exists_in_timeline",
|
|
1029
|
+
},
|
|
1030
|
+
warnings: [],
|
|
1031
|
+
sourceRefs: [],
|
|
1032
|
+
};
|
|
1033
|
+
return envelope;
|
|
1034
|
+
}
|
|
1035
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1036
|
+
const envelope = {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
command: "narrative:diff",
|
|
1039
|
+
runtimeMode: "unavailable",
|
|
1040
|
+
surfaceMode: "cli",
|
|
1041
|
+
generatedAt,
|
|
1042
|
+
error: { code: "NARRATIVE_DIFF_FAILED", message: msg },
|
|
1043
|
+
warnings: [],
|
|
1044
|
+
sourceRefs: [],
|
|
1045
|
+
};
|
|
1046
|
+
return envelope;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* [G6] timeline — queryNarrativeTimeline with cursor pagination.
|
|
1051
|
+
* Requires narrativeTimelineDeps in OpsRouterDeps.
|
|
1052
|
+
*/
|
|
1053
|
+
if (command === "timeline") {
|
|
1054
|
+
const generatedAt = new Date().toISOString();
|
|
1055
|
+
if (!deps.narrativeTimelineDeps) {
|
|
1056
|
+
const envelope = {
|
|
1057
|
+
ok: false,
|
|
1058
|
+
command: "timeline",
|
|
1059
|
+
runtimeMode: "unavailable",
|
|
1060
|
+
surfaceMode: "cli",
|
|
1061
|
+
generatedAt,
|
|
1062
|
+
error: {
|
|
1063
|
+
code: "NARRATIVE_TIMELINE_PORT_UNAVAILABLE",
|
|
1064
|
+
message: "timeline requires narrativeTimelineDeps in OpsRouterDeps",
|
|
1065
|
+
nextStep: "wire_narrative_timeline_deps_into_ops_router",
|
|
1066
|
+
},
|
|
1067
|
+
warnings: [],
|
|
1068
|
+
sourceRefs: [],
|
|
1069
|
+
};
|
|
1070
|
+
return envelope;
|
|
1071
|
+
}
|
|
1072
|
+
const now = new Date();
|
|
1073
|
+
const to = typeof input?.to === "string" ? input.to : now.toISOString();
|
|
1074
|
+
const from = typeof input?.from === "string"
|
|
1075
|
+
? input.from
|
|
1076
|
+
: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
1077
|
+
const limit = typeof input?.limit === "number" ? input.limit : 20;
|
|
1078
|
+
const cursor = typeof input?.cursor === "string" ? input.cursor : undefined;
|
|
1079
|
+
try {
|
|
1080
|
+
const page = await queryNarrativeTimeline(from, to, { limit, cursor }, deps.narrativeTimelineDeps);
|
|
1081
|
+
const envelope = {
|
|
1082
|
+
ok: true,
|
|
1083
|
+
command: "timeline",
|
|
1084
|
+
runtimeMode: "workspace_full_runtime",
|
|
1085
|
+
surfaceMode: "cli",
|
|
1086
|
+
generatedAt,
|
|
1087
|
+
data: page,
|
|
1088
|
+
warnings: [],
|
|
1089
|
+
sourceRefs: ["observability/services/narrative-timeline-query-service.ts"],
|
|
1090
|
+
};
|
|
1091
|
+
return envelope;
|
|
1092
|
+
}
|
|
1093
|
+
catch (err) {
|
|
1094
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1095
|
+
const code = err.name === "NarrativeQueryRangeError"
|
|
1096
|
+
? "NARRATIVE_RANGE_EXCEEDED"
|
|
1097
|
+
: "TIMELINE_QUERY_FAILED";
|
|
1098
|
+
const envelope = {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
command: "timeline",
|
|
1101
|
+
runtimeMode: "unavailable",
|
|
1102
|
+
surfaceMode: "cli",
|
|
1103
|
+
generatedAt,
|
|
1104
|
+
error: { code, message: msg },
|
|
1105
|
+
warnings: [],
|
|
1106
|
+
sourceRefs: [],
|
|
1107
|
+
};
|
|
1108
|
+
return envelope;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* [G6] restore — bounded state restoration via RestoreSnapshotStore + audit (T-ROS.C.1, T-OBS.C.6).
|
|
1113
|
+
* When restoreSnapshotStore is wired, attempts to apply the snapshot payload back to state.
|
|
1114
|
+
* Always writes RestoreAudit. Never restores credential fields.
|
|
1115
|
+
*/
|
|
1116
|
+
if (command === "restore") {
|
|
1117
|
+
const generatedAt = new Date().toISOString();
|
|
1118
|
+
if (!deps.auditStore) {
|
|
1119
|
+
const envelope = {
|
|
1120
|
+
ok: false,
|
|
1121
|
+
command: "restore",
|
|
1122
|
+
runtimeMode: "unavailable",
|
|
1123
|
+
surfaceMode: "cli",
|
|
1124
|
+
generatedAt,
|
|
1125
|
+
error: {
|
|
1126
|
+
code: "AUDIT_STORE_UNAVAILABLE",
|
|
1127
|
+
message: "restore requires auditStore in OpsRouterDeps",
|
|
1128
|
+
nextStep: "wire_audit_store_into_ops_router",
|
|
1129
|
+
},
|
|
1130
|
+
warnings: [],
|
|
1131
|
+
sourceRefs: [],
|
|
1132
|
+
};
|
|
1133
|
+
return envelope;
|
|
1134
|
+
}
|
|
1135
|
+
let restoreTarget;
|
|
1136
|
+
let fromVersion;
|
|
1137
|
+
let toVersion;
|
|
1138
|
+
// T-V7C.C.5: snapshotId operator-friendly parameter takes precedence over legacy fields.
|
|
1139
|
+
// When snapshotId is provided, resolve restoreTarget/fromVersion/toVersion from the
|
|
1140
|
+
// matching snapshot row; otherwise fall back to explicit legacy parameters.
|
|
1141
|
+
const snapshotId = textInput(input, "snapshotId");
|
|
1142
|
+
if (snapshotId) {
|
|
1143
|
+
if (!deps.restoreSnapshotStore) {
|
|
1144
|
+
const envelope = {
|
|
1145
|
+
ok: false,
|
|
1146
|
+
command: "restore",
|
|
1147
|
+
runtimeMode: "unavailable",
|
|
1148
|
+
surfaceMode: "cli",
|
|
1149
|
+
generatedAt,
|
|
1150
|
+
error: {
|
|
1151
|
+
code: "RESTORE_SNAPSHOT_STORE_UNAVAILABLE",
|
|
1152
|
+
message: "snapshotId restore requires restoreSnapshotStore in OpsRouterDeps",
|
|
1153
|
+
nextStep: "wire_restore_snapshot_store_into_ops_router",
|
|
1154
|
+
},
|
|
1155
|
+
warnings: [],
|
|
1156
|
+
sourceRefs: [],
|
|
1157
|
+
};
|
|
1158
|
+
return envelope;
|
|
1159
|
+
}
|
|
1160
|
+
const snapshots = await deps.restoreSnapshotStore.listSnapshots();
|
|
1161
|
+
const match = snapshots.find((s) => s.snapshotId === snapshotId);
|
|
1162
|
+
if (match) {
|
|
1163
|
+
restoreTarget = snapshotId;
|
|
1164
|
+
fromVersion = match.capturedAt;
|
|
1165
|
+
toVersion = snapshotId;
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const envelope = {
|
|
1169
|
+
ok: false,
|
|
1170
|
+
command: "restore",
|
|
1171
|
+
runtimeMode: "workspace_full_runtime",
|
|
1172
|
+
surfaceMode: "cli",
|
|
1173
|
+
generatedAt,
|
|
1174
|
+
error: {
|
|
1175
|
+
code: "SNAPSHOT_NOT_FOUND",
|
|
1176
|
+
message: `snapshotId ${snapshotId} not found in restore_snapshot table`,
|
|
1177
|
+
nextStep: "list_available_snapshots_or_verify_snapshotId",
|
|
1178
|
+
},
|
|
1179
|
+
warnings: [],
|
|
1180
|
+
sourceRefs: [],
|
|
1181
|
+
};
|
|
1182
|
+
return envelope;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
const missingFields = [];
|
|
1187
|
+
if (typeof input?.restoreTarget !== "string")
|
|
1188
|
+
missingFields.push("restoreTarget");
|
|
1189
|
+
if (typeof input?.fromVersion !== "string")
|
|
1190
|
+
missingFields.push("fromVersion");
|
|
1191
|
+
if (typeof input?.toVersion !== "string")
|
|
1192
|
+
missingFields.push("toVersion");
|
|
1193
|
+
if (missingFields.length > 0) {
|
|
1194
|
+
const envelope = {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
command: "restore",
|
|
1197
|
+
runtimeMode: "workspace_full_runtime",
|
|
1198
|
+
surfaceMode: "cli",
|
|
1199
|
+
generatedAt,
|
|
1200
|
+
error: {
|
|
1201
|
+
code: "MISSING_RESTORE_FIELDS",
|
|
1202
|
+
message: `restore requires: ${missingFields.join(", ")}`,
|
|
1203
|
+
nextStep: "reinvoke_with_required_fields",
|
|
1204
|
+
},
|
|
1205
|
+
warnings: [],
|
|
1206
|
+
sourceRefs: [],
|
|
1207
|
+
};
|
|
1208
|
+
return envelope;
|
|
1209
|
+
}
|
|
1210
|
+
restoreTarget = input.restoreTarget;
|
|
1211
|
+
fromVersion = input.fromVersion;
|
|
1212
|
+
toVersion = input.toVersion;
|
|
1213
|
+
}
|
|
1214
|
+
// [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
|
|
1215
|
+
let restoreResult = {
|
|
1216
|
+
ok: false,
|
|
1217
|
+
completedEntities: [],
|
|
1218
|
+
failedEntities: [],
|
|
1219
|
+
warnings: ["restore_snapshot_store_unavailable"],
|
|
1220
|
+
};
|
|
1221
|
+
if (deps.restoreSnapshotStore) {
|
|
1222
|
+
restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
|
|
1223
|
+
restoreTarget: restoreTarget,
|
|
1224
|
+
fromVersion: fromVersion,
|
|
1225
|
+
toVersion: toVersion,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
const event = {
|
|
1229
|
+
id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1230
|
+
restoreTarget: restoreTarget,
|
|
1231
|
+
fromVersion: fromVersion,
|
|
1232
|
+
toVersion: toVersion,
|
|
1233
|
+
triggeredBy: input?.triggeredBy ?? "operator",
|
|
1234
|
+
reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
|
|
1235
|
+
completedEntities: restoreResult.completedEntities,
|
|
1236
|
+
failedEntities: restoreResult.failedEntities,
|
|
1237
|
+
// credentials are always excluded from restore audit
|
|
1238
|
+
excludedFields: Array.isArray(input?.excludedFields)
|
|
1239
|
+
? input.excludedFields.filter((f) => typeof f === "string")
|
|
1240
|
+
: ["credential", "encryptionKey"],
|
|
1241
|
+
restoredFieldCount: restoreResult.completedEntities.length,
|
|
1242
|
+
createdAt: generatedAt,
|
|
1243
|
+
traceId: typeof input?.traceId === "string" ? input.traceId : `trace-restore-${Date.now()}`,
|
|
1244
|
+
};
|
|
1245
|
+
const auditResult = await writeRestoreAudit(event, deps.auditStore);
|
|
1246
|
+
const envelope = {
|
|
1247
|
+
ok: restoreResult.ok && auditResult.ok,
|
|
1248
|
+
command: "restore",
|
|
1249
|
+
runtimeMode: "workspace_full_runtime",
|
|
1250
|
+
surfaceMode: "cli",
|
|
1251
|
+
generatedAt,
|
|
1252
|
+
data: {
|
|
1253
|
+
auditWritten: auditResult.warnings.length === 0,
|
|
1254
|
+
fromVersion: event.fromVersion,
|
|
1255
|
+
toVersion: event.toVersion,
|
|
1256
|
+
restoreTarget: event.restoreTarget,
|
|
1257
|
+
isPartialRestore: event.failedEntities.length > 0,
|
|
1258
|
+
failedEntities: event.failedEntities,
|
|
1259
|
+
completedEntities: event.completedEntities,
|
|
1260
|
+
restoreSnapshotStoreAvailable: !!deps.restoreSnapshotStore,
|
|
1261
|
+
},
|
|
1262
|
+
warnings: [...restoreResult.warnings, ...auditResult.warnings],
|
|
1263
|
+
sourceRefs: [
|
|
1264
|
+
"observability/services/restore-audit-service.ts",
|
|
1265
|
+
"storage/services/restore-snapshot-store.ts",
|
|
1266
|
+
],
|
|
1267
|
+
};
|
|
1268
|
+
return envelope;
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* [G7] runtime_secret_bootstrap — RuntimeSecretAnchorView pass-through.
|
|
1272
|
+
* Requires secretAnchorDeps in OpsRouterDeps; never returns key plaintext.
|
|
1273
|
+
*/
|
|
1274
|
+
if (command === "runtime_secret_bootstrap") {
|
|
1275
|
+
const generatedAt = new Date().toISOString();
|
|
1276
|
+
if (!deps.secretAnchorDeps) {
|
|
1277
|
+
const envelope = {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
command: "runtime_secret_bootstrap",
|
|
1280
|
+
runtimeMode: "unavailable",
|
|
1281
|
+
surfaceMode: "cli",
|
|
1282
|
+
generatedAt,
|
|
1283
|
+
error: {
|
|
1284
|
+
code: "SECRET_ANCHOR_DEPS_UNAVAILABLE",
|
|
1285
|
+
message: "runtime_secret_bootstrap requires secretAnchorDeps in OpsRouterDeps",
|
|
1286
|
+
nextStep: "wire_secret_anchor_deps_into_ops_router",
|
|
1287
|
+
},
|
|
1288
|
+
warnings: [],
|
|
1289
|
+
sourceRefs: [],
|
|
1290
|
+
};
|
|
1291
|
+
return envelope;
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
const view = await viewSecretAnchor(deps.secretAnchorDeps);
|
|
1295
|
+
// Map to RuntimeSecretBootstrapView (design model §6.1)
|
|
1296
|
+
const data = {
|
|
1297
|
+
status: view.status === "verified" || view.status === "ok"
|
|
1298
|
+
? "ok"
|
|
1299
|
+
: view.status === "missing"
|
|
1300
|
+
? "runtime_secret_anchor_missing"
|
|
1301
|
+
: view.status === "wrong_key"
|
|
1302
|
+
? "credential_recovery_required"
|
|
1303
|
+
: view.status === "decryption_failed"
|
|
1304
|
+
? "runtime_secret_unavailable"
|
|
1305
|
+
: "unknown",
|
|
1306
|
+
keyHealth: view.status === "verified" || view.status === "ok"
|
|
1307
|
+
? "ok"
|
|
1308
|
+
: view.status === "missing"
|
|
1309
|
+
? "missing_key"
|
|
1310
|
+
: view.status === "wrong_key"
|
|
1311
|
+
? "wrong_key"
|
|
1312
|
+
: "unknown",
|
|
1313
|
+
anchorLocation: view.keyPath,
|
|
1314
|
+
recoveryPrincipleRef: view.recoveryDocRef,
|
|
1315
|
+
plaintextKeyExposed: false,
|
|
1316
|
+
reasonCode: view.reasonCode,
|
|
1317
|
+
recoverySteps: view.recoverySteps,
|
|
1318
|
+
};
|
|
1319
|
+
const envelope = {
|
|
1320
|
+
ok: true,
|
|
1321
|
+
command: "runtime_secret_bootstrap",
|
|
1322
|
+
runtimeMode: "workspace_full_runtime",
|
|
1323
|
+
surfaceMode: "cli",
|
|
1324
|
+
generatedAt,
|
|
1325
|
+
data,
|
|
1326
|
+
warnings: [],
|
|
1327
|
+
sourceRefs: ["observability/services/runtime-secret-anchor-view.ts"],
|
|
1328
|
+
};
|
|
1329
|
+
return envelope;
|
|
1330
|
+
}
|
|
1331
|
+
catch (err) {
|
|
1332
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1333
|
+
const envelope = {
|
|
1334
|
+
ok: false,
|
|
1335
|
+
command: "runtime_secret_bootstrap",
|
|
1336
|
+
runtimeMode: "unavailable",
|
|
1337
|
+
surfaceMode: "cli",
|
|
1338
|
+
generatedAt,
|
|
1339
|
+
error: { code: "SECRET_ANCHOR_PROBE_FAILED", message: msg },
|
|
1340
|
+
warnings: [],
|
|
1341
|
+
sourceRefs: [],
|
|
1342
|
+
};
|
|
1343
|
+
return envelope;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
// ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
|
|
1347
|
+
// Returns the assembled impulse + atmosphere for a given scene context.
|
|
1348
|
+
// Useful for Claw to inspect what guidance content would be injected before
|
|
1349
|
+
// a real heartbeat cycle, and to verify platform-specific impulse overrides.
|
|
1350
|
+
if (command === "guidance_payload") {
|
|
1351
|
+
const generatedAt = new Date().toISOString();
|
|
1352
|
+
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1353
|
+
const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1354
|
+
const sceneType = input?.sceneType ?? "social";
|
|
1355
|
+
const capabilityIntent = typeof input?.capabilityIntent === "string"
|
|
1356
|
+
? input.capabilityIntent
|
|
1357
|
+
: undefined;
|
|
1358
|
+
const platformId = typeof input?.platformId === "string"
|
|
1359
|
+
? input.platformId
|
|
1360
|
+
: undefined;
|
|
1361
|
+
const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
|
|
1362
|
+
if (!validSceneTypes.includes(sceneType)) {
|
|
1363
|
+
const envelope = {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
command: "guidance_payload",
|
|
1366
|
+
runtimeMode: "unavailable",
|
|
1367
|
+
surfaceMode: "cli",
|
|
1368
|
+
generatedAt,
|
|
1369
|
+
error: {
|
|
1370
|
+
code: "INVALID_SCENE_TYPE",
|
|
1371
|
+
message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
|
|
1372
|
+
nextStep: "reinvoke_with_valid_scene_type",
|
|
1373
|
+
},
|
|
1374
|
+
warnings: [],
|
|
1375
|
+
sourceRefs: [],
|
|
1376
|
+
};
|
|
1377
|
+
return envelope;
|
|
1378
|
+
}
|
|
1379
|
+
const impulseResult = assembleImpulseSync({
|
|
1380
|
+
sceneType: sceneType,
|
|
1381
|
+
capabilityIntent,
|
|
1382
|
+
platformId,
|
|
1383
|
+
});
|
|
1384
|
+
const { buildExpressionBoundary } = await import("../../guidance/output-guard.js");
|
|
1385
|
+
const { getShortAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1386
|
+
const atmosphere = getShortAtmosphereTemplate("active", "low");
|
|
1387
|
+
const expressionBoundary = buildExpressionBoundary(sceneType);
|
|
1388
|
+
const envelope = {
|
|
1389
|
+
ok: true,
|
|
1390
|
+
command: "guidance_payload",
|
|
1391
|
+
runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
|
|
1392
|
+
surfaceMode: "cli",
|
|
1393
|
+
generatedAt,
|
|
1394
|
+
data: {
|
|
1395
|
+
sceneType,
|
|
1396
|
+
capabilityIntent: capabilityIntent ?? null,
|
|
1397
|
+
platformId: platformId ?? null,
|
|
1398
|
+
capabilityClass: impulseResult.capabilityClass,
|
|
1399
|
+
impulseSource: impulseResult.source,
|
|
1400
|
+
impulseText: impulseResult.impulse?.text ?? null,
|
|
1401
|
+
impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
|
|
1402
|
+
atmosphereText: atmosphere.text,
|
|
1403
|
+
atmosphereReviewStatus: atmosphere.reviewStatus,
|
|
1404
|
+
expressionBoundaryConstraints: expressionBoundary.constraints,
|
|
1405
|
+
expressionBoundaryStyle: expressionBoundary.style,
|
|
1406
|
+
},
|
|
1407
|
+
warnings: impulseResult.source === "none"
|
|
1408
|
+
? ["no_impulse_available_for_this_scene_and_capability"]
|
|
1409
|
+
: [],
|
|
1410
|
+
sourceRefs: [
|
|
1411
|
+
"guidance/capability-class.ts",
|
|
1412
|
+
"guidance/impulse-assembler.ts",
|
|
1413
|
+
"guidance/template-registry.ts",
|
|
1414
|
+
"guidance/output-guard.ts",
|
|
1415
|
+
],
|
|
1416
|
+
};
|
|
1417
|
+
return envelope;
|
|
1418
|
+
}
|
|
1419
|
+
return {
|
|
1420
|
+
ok: false,
|
|
1421
|
+
error: {
|
|
1422
|
+
code: "unknown_ops_command",
|
|
1423
|
+
message: `Unknown ops command: ${command}`,
|
|
1424
|
+
},
|
|
1425
|
+
};
|
|
1426
|
+
},
|
|
1427
|
+
};
|
|
1428
|
+
}
|