@haaaiawd/second-nature 0.1.38 → 0.1.40

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.
Files changed (50) hide show
  1. package/agent-inner-guide.md +18 -0
  2. package/index.js +10 -2
  3. package/openclaw.plugin.json +2 -2
  4. package/package.json +1 -1
  5. package/runtime/cli/commands/connector-init.js +11 -4
  6. package/runtime/cli/index.js +6 -1
  7. package/runtime/cli/ops/heartbeat-surface.d.ts +15 -0
  8. package/runtime/cli/ops/heartbeat-surface.js +16 -2
  9. package/runtime/cli/ops/ops-router.js +229 -83
  10. package/runtime/cli/ops/workspace-heartbeat-runner.js +49 -4
  11. package/runtime/connectors/services/connector-executor-adapter.js +192 -41
  12. package/runtime/core/second-nature/guidance/apply-guidance.d.ts +2 -0
  13. package/runtime/core/second-nature/guidance/apply-guidance.js +6 -1
  14. package/runtime/core/second-nature/guidance/user-reply-continuity.d.ts +1 -1
  15. package/runtime/core/second-nature/guidance/user-reply-continuity.js +14 -5
  16. package/runtime/core/second-nature/orchestrator/intent-planner.js +15 -0
  17. package/runtime/core/second-nature/runtime/service-entry.d.ts +3 -0
  18. package/runtime/core/second-nature/runtime/service-entry.js +1 -2
  19. package/runtime/dream/dream-engine.d.ts +14 -0
  20. package/runtime/dream/dream-engine.js +306 -0
  21. package/runtime/dream/dream-input-loader.d.ts +37 -0
  22. package/runtime/dream/dream-input-loader.js +155 -0
  23. package/runtime/dream/dream-scheduler.d.ts +75 -0
  24. package/runtime/dream/dream-scheduler.js +131 -0
  25. package/runtime/dream/index.d.ts +16 -0
  26. package/runtime/dream/index.js +14 -0
  27. package/runtime/dream/insight-extractor.d.ts +32 -0
  28. package/runtime/dream/insight-extractor.js +135 -0
  29. package/runtime/dream/memory-consolidator.d.ts +45 -0
  30. package/runtime/dream/memory-consolidator.js +140 -0
  31. package/runtime/dream/narrative-update-proposal.d.ts +34 -0
  32. package/runtime/dream/narrative-update-proposal.js +83 -0
  33. package/runtime/dream/output-validator.d.ts +20 -0
  34. package/runtime/dream/output-validator.js +110 -0
  35. package/runtime/dream/redaction-gate.d.ts +31 -0
  36. package/runtime/dream/redaction-gate.js +109 -0
  37. package/runtime/dream/relationship-update-proposal.d.ts +27 -0
  38. package/runtime/dream/relationship-update-proposal.js +119 -0
  39. package/runtime/dream/sampler.d.ts +30 -0
  40. package/runtime/dream/sampler.js +65 -0
  41. package/runtime/dream/types.d.ts +187 -0
  42. package/runtime/dream/types.js +11 -0
  43. package/runtime/guidance/fallback.js +6 -3
  44. package/runtime/guidance/guidance-assembler.js +5 -3
  45. package/runtime/guidance/output-guard.d.ts +4 -1
  46. package/runtime/guidance/output-guard.js +24 -0
  47. package/runtime/guidance/template-registry.d.ts +5 -1
  48. package/runtime/guidance/template-registry.js +71 -30
  49. package/runtime/guidance/types.d.ts +14 -0
  50. package/runtime/observability/projections/guidance-audit.js +4 -1
@@ -167,3 +167,21 @@ Second Nature 在一些特定动作发生之前,会给你一个简短的内心
167
167
  定制 impulse 不需要很长。一到三段,把这个平台在这个动作上的具体质感讲清楚就够了。
168
168
 
169
169
  格式和内置 impulse 一致——前置 frontmatter,正文纯文字,第一人称"我",不带标题,不带列表。
170
+
171
+ ## expression boundary 与 output guard
172
+
173
+ Second Nature 会给你一组表达边界(expression boundary),用来提醒你避免某些腔调或表达陷阱。
174
+
175
+ 它不是格式规范,也不是硬 guard 的 verdict。它只负责一件事:**让你的表达更像你自己,而不是像某个模板、客服系统或日报机器。**
176
+
177
+ 比如它会提醒你:
178
+
179
+ - 避免客服腔或通知系统语气
180
+ - 避免日报、周报或例行播报腔
181
+ - 避免教程、步骤说明或培训手册腔
182
+ - 不要虚构经历、关系、情绪事件
183
+ - 不要滑进高重复、硬模板化的措辞
184
+
185
+ 这些提醒不会替你决定「能不能做这件事」——能不能做由 hard guard 单独判断。expression boundary 只在你已经决定要做之后,轻轻 shaping 一下你的表达方式。
186
+
187
+ 你也不需要把它当成必须逐条遵守的评分表。它只是空气里的一道微弱边界:当你快要滑进某种腔调时,你会感觉到它在。
package/index.js CHANGED
@@ -55,6 +55,8 @@
55
55
  import fs from "node:fs";
56
56
  import path from "node:path";
57
57
  import { fileURLToPath } from "node:url";
58
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
59
+ const PLUGIN_VERSION = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8")).version;
58
60
  import { startRuntimeService, } from "./runtime/core/second-nature/runtime/service-entry.js";
59
61
  import { getLifecycleState, recordRegistration, } from "./runtime/core/second-nature/runtime/lifecycle-service.js";
60
62
  import { openWorkspaceOpsBridge } from "./workspace-ops-bridge.js";
@@ -106,8 +108,9 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
106
108
  "connector_test",
107
109
  "connector_behavior_add",
108
110
  "cycle:recent",
109
- // v7 ops surface (T-ROS.C.1 / T-ROS.C.2 / T-ROS.C.3): self_health, tool_affordance, heartbeat_digest,
110
- // narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run
111
+ // v7 ops surface (T-ROS.C.1 / T-ROS.C.2 / T-ROS.C.3 / T-V7C.C.5): self_health, tool_affordance,
112
+ // heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap,
113
+ // connector:run, guidance_payload
111
114
  "self_health",
112
115
  "tool_affordance",
113
116
  "heartbeat_digest",
@@ -117,6 +120,8 @@ const WORKSPACE_BRIDGE_COMMANDS = new Set([
117
120
  "restore",
118
121
  "runtime_secret_bootstrap",
119
122
  "connector:run",
123
+ // T-V7C.C.5: host ops surface parity — guidance_payload must be whitelisted for Claw reachability
124
+ "guidance_payload",
120
125
  ]);
121
126
  function isWorkspaceBridgeCommand(command, input) {
122
127
  if (command === "credential") {
@@ -193,6 +198,7 @@ function syncWorkspaceRootFromTool(spine, toolWorkspaceRoot) {
193
198
  if (changed) {
194
199
  spine.runtimeHandle = startRuntimeService({
195
200
  workspaceRoot: next.runtimeRoot,
201
+ version: PLUGIN_VERSION,
196
202
  });
197
203
  }
198
204
  }
@@ -885,6 +891,7 @@ function createActivationSpine() {
885
891
  router: undefined,
886
892
  runtimeHandle: startRuntimeService({
887
893
  workspaceRoot: workspaceRootContext.runtimeRoot,
894
+ version: PLUGIN_VERSION,
888
895
  }),
889
896
  lifecycleState: getLifecycleState(),
890
897
  serviceStartRecorded: false,
@@ -933,6 +940,7 @@ function refreshRegistrationState() {
933
940
  spine.workspaceRootContext = workspaceRootContext;
934
941
  spine.runtimeHandle = startRuntimeService({
935
942
  workspaceRoot: workspaceRootContext.runtimeRoot,
943
+ version: PLUGIN_VERSION,
936
944
  });
937
945
  spine.lifecycleState = recordRegistration();
938
946
  spine.serviceStartRecorded = false;
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.38",
5
- "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap.",
4
+ "version": "0.1.40",
5
+ "description": "OpenClaw native plugin with synchronous surface registration and bundled runtime spine. Set SECOND_NATURE_WORKSPACE_ROOT or tool workspaceRoot to the same path as the agent workspace. Agent inner guide is packaged as agent-inner-guide.md. v7 ops surface: self_health, tool_affordance, heartbeat_digest, snapshot:capture, narrative:diff, timeline, restore, runtime_secret_bootstrap, connector:run, guidance_payload.",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "OpenClaw native plugin with synchronous registration, a packaged runtime artifact, and operator-facing status/explain flows.",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -11,23 +11,30 @@ function generateManifestYaml(input) {
11
11
  const displayName = input.displayName ?? platformId;
12
12
  const family = input.family ?? "custom";
13
13
  const runnerKind = input.runnerKind ?? "declarative_http";
14
- const baseUrlLine = input.baseUrl ? `\nbaseUrl: ${input.baseUrl}` : "";
14
+ const trustStatus = runnerKind === "declarative_http" ||
15
+ runnerKind === "declarative_a2a" ||
16
+ runnerKind === "declarative_mcp"
17
+ ? "declarative_trusted"
18
+ : "custom_adapter_pending_trust";
19
+ const configBlock = input.baseUrl
20
+ ? `\n config:\n baseUrl: ${input.baseUrl}`
21
+ : "";
15
22
  return `schemaVersion: sn.connector.v1
16
23
  platformId: ${platformId}
17
24
  displayName: ${displayName}
18
- family: ${family}${baseUrlLine}
25
+ family: ${family}
19
26
  capabilities:
20
27
  - id: ${platformId}.placeholder
21
28
  description: Placeholder capability — replace with real capability declarations
22
29
  runner:
23
30
  kind: ${runnerKind}
24
- entrypoint: ""
31
+ entrypoint: ""${configBlock}
25
32
  credentials: []
26
33
  sourceRefPolicy:
27
34
  minSourceRefs: 1
28
35
  rejectInlineSensitivePayload: true
29
36
  trust:
30
- status: custom_adapter_pending_trust
37
+ status: ${trustStatus}
31
38
  reason: generated_by_connector_init
32
39
  `;
33
40
  }
@@ -113,7 +113,12 @@ function createWorkspaceAffordanceAssembler(registry, workspaceRoot) {
113
113
  return undefined;
114
114
  },
115
115
  },
116
- credentialRequired: () => false,
116
+ // W80: built-in connectors without probe history should posture as
117
+ // "needs_auth" (guard allows) rather than "unavailable" (guard defers).
118
+ credentialRequired: (platformId) => {
119
+ const builtInPlatforms = new Set(["moltbook", "evomap", "agent-world", "instreet"]);
120
+ return builtInPlatforms.has(platformId);
121
+ },
117
122
  });
118
123
  }
119
124
  function stringifyDeltaField(delta, key) {
@@ -13,6 +13,8 @@ import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/ef
13
13
  import type { CapabilityContractRegistry } from "../../connectors/base/manifest.js";
14
14
  import type { AffordanceMap } from "../../shared/types/v7-entities.js";
15
15
  import type { ExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
16
+ import type { QuietDreamSchedulePort } from "../../core/second-nature/quiet/run-source-backed-quiet.js";
17
+ import type { HeartbeatDigestAssemblerDeps } from "../../observability/services/heartbeat-digest-assembler.js";
16
18
  export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
17
19
  export interface HeartbeatSurfaceResult {
18
20
  ok: boolean;
@@ -56,5 +58,18 @@ export interface HeartbeatCheckInput {
56
58
  affordanceMap?: AffordanceMap;
57
59
  /** v7 T-V7C.C.2: experience writer for heartbeat connector attempts. */
58
60
  experienceWriter?: ExperienceWriter;
61
+ /**
62
+ * v7 T-V7C.C.6: when present, a successful Quiet write auto-triggers Dream scheduling.
63
+ * Fixes the production-data gap where dream_output_index does not grow after Quiet.
64
+ */
65
+ dreamSchedulePort?: QuietDreamSchedulePort;
66
+ /**
67
+ * v7 T-V7C.C.6: when present, generates a HeartbeatDigest after each cycle.
68
+ * Fixes the production-data gap where heartbeat_digest does not grow.
69
+ */
70
+ digestOpts?: {
71
+ assemblerDeps: HeartbeatDigestAssemblerDeps;
72
+ digestWindowHour?: number;
73
+ };
59
74
  }
60
75
  export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
@@ -77,7 +77,21 @@ export async function heartbeatCheck(input) {
77
77
  connectorRegistry: input.connectorRegistry,
78
78
  affordanceMap: input.affordanceMap,
79
79
  experienceWriter: input.experienceWriter,
80
+ dreamSchedulePort: input.dreamSchedulePort,
81
+ digestOpts: input.digestOpts,
80
82
  });
81
- const cycle = await run(signal);
82
- return mapCycleToSurface(cycle, "workspace_full_runtime");
83
+ try {
84
+ const cycle = await run(signal);
85
+ return mapCycleToSurface(cycle, "workspace_full_runtime");
86
+ }
87
+ catch (err) {
88
+ const msg = err instanceof Error ? err.message : String(err);
89
+ return {
90
+ ok: false,
91
+ status: "denied",
92
+ surfaceMode: "workspace_full_runtime",
93
+ reasons: [`heartbeat_cycle_exception:${msg.slice(0, 120)}`],
94
+ livedExperienceLoopClaimed: false,
95
+ };
96
+ }
83
97
  }
@@ -30,10 +30,54 @@ import { createExperienceWriter } from "../../core/second-nature/body/tool-exper
30
30
  import { createCapabilityProbeResultStore, createToolExperienceStore, } from "../../storage/services/tool-experience-store.js";
31
31
  import { createWetProbeRunner } from "../../connectors/base/wet-probe-runner.js";
32
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";
33
37
  function coerceProbeOnlyFlag(input) {
34
38
  const v = input?.probeOnly;
35
39
  return v === true || v === "true" || v === 1 || v === "1";
36
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
+ }
37
81
  const SNAPSHOT_TABLE_BY_KIND = {
38
82
  identity_profile: "identity_profile",
39
83
  agent_goal: "agent_goal",
@@ -278,6 +322,8 @@ export function createOpsRouter(deps) {
278
322
  connectorExecutor: input.connectorExecutor ?? deps.connectorExecutor,
279
323
  connectorRegistry: input
280
324
  ?.connectorRegistry ?? deps.connectorRegistry,
325
+ digestOpts: input.digestOpts,
326
+ dreamSchedulePort: input.dreamSchedulePort,
281
327
  }),
282
328
  async dispatch(command, input) {
283
329
  if (command === "heartbeat_check") {
@@ -298,61 +344,98 @@ export function createOpsRouter(deps) {
298
344
  if (deps.state) {
299
345
  experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
300
346
  }
301
- const result = await heartbeatCheck({
302
- probeOnly: coerceProbeOnlyFlag(input),
303
- runtimeAvailable,
304
- fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
305
- typeof input.fakeControlPlanePassthrough === "object"
306
- ? input.fakeControlPlanePassthrough
307
- : undefined,
308
- readModels: input?.readModels ??
309
- deps.readModels,
310
- runtimeRecorder: input
311
- ?.runtimeRecorder ?? deps.runtimeRecorder,
312
- state: input?.state ??
313
- deps.state,
314
- workspaceRoot: input
315
- ?.workspaceRoot ?? deps.workspaceRoot,
316
- timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
317
- sessionContext: typeof input?.sessionContext === "string"
318
- ? input.sessionContext
319
- : undefined,
320
- scopeHint: input?.scopeHint,
321
- connectorExecutor: input
322
- ?.connectorExecutor ?? deps.connectorExecutor,
323
- connectorRegistry: input
324
- ?.connectorRegistry ?? deps.connectorRegistry,
325
- affordanceMap,
326
- experienceWriter,
327
- });
328
- if (result.ok &&
329
- result.surfaceMode === "workspace_full_runtime" &&
330
- !coerceProbeOnlyFlag(input) &&
331
- deps.state &&
332
- deps.restoreSnapshotStore) {
333
- try {
334
- const capture = await captureRuntimeSnapshot(deps, {
335
- snapshotId: `heartbeat:${result.decisionId ?? "cycle"}:${Date.now()}`,
336
- subjectId: result.decisionId ?? "heartbeat_check",
337
- reasonCode: "heartbeat_check",
338
- summaryText: `Heartbeat ${result.status} captured bounded restore snapshot`,
339
- focus: result.status,
340
- progress: result.reasons.join(",") || "heartbeat_completed",
341
- nextIntent: "continue_runtime_loop",
342
- sourceRefs: result.decisionId
343
- ? [`heartbeat:${result.decisionId}`]
344
- : ["heartbeat:runtime"],
345
- });
346
- if (capture.ok) {
347
- result.reasons = [...result.reasons, "restore_snapshot_captured"];
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}`];
348
417
  }
349
418
  }
350
- catch (err) {
351
- const msg = err instanceof Error ? err.message : String(err);
352
- result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
353
- }
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;
354
438
  }
355
- return result;
356
439
  }
357
440
  if (command === "fallback") {
358
441
  const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
@@ -569,7 +652,9 @@ export function createOpsRouter(deps) {
569
652
  warnings.push("state_db_unavailable:capability_probe_result_not_persisted");
570
653
  }
571
654
  return {
572
- ok: wetResult.probeResult.actualStatus !== "unavailable",
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",
573
658
  command: "connector_test",
574
659
  data: {
575
660
  ...data,
@@ -1047,29 +1132,84 @@ export function createOpsRouter(deps) {
1047
1132
  };
1048
1133
  return envelope;
1049
1134
  }
1050
- const missingFields = [];
1051
- if (typeof input?.restoreTarget !== "string")
1052
- missingFields.push("restoreTarget");
1053
- if (typeof input?.fromVersion !== "string")
1054
- missingFields.push("fromVersion");
1055
- if (typeof input?.toVersion !== "string")
1056
- missingFields.push("toVersion");
1057
- if (missingFields.length > 0) {
1058
- const envelope = {
1059
- ok: false,
1060
- command: "restore",
1061
- runtimeMode: "workspace_full_runtime",
1062
- surfaceMode: "cli",
1063
- generatedAt,
1064
- error: {
1065
- code: "MISSING_RESTORE_FIELDS",
1066
- message: `restore requires: ${missingFields.join(", ")}`,
1067
- nextStep: "reinvoke_with_required_fields",
1068
- },
1069
- warnings: [],
1070
- sourceRefs: [],
1071
- };
1072
- return envelope;
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;
1073
1213
  }
1074
1214
  // [NEW] Invoke bounded restore via RestoreSnapshotStore when wired
1075
1215
  let restoreResult = {
@@ -1080,16 +1220,16 @@ export function createOpsRouter(deps) {
1080
1220
  };
1081
1221
  if (deps.restoreSnapshotStore) {
1082
1222
  restoreResult = await deps.restoreSnapshotStore.applyBoundedRestore({
1083
- restoreTarget: input.restoreTarget,
1084
- fromVersion: input.fromVersion,
1085
- toVersion: input.toVersion,
1223
+ restoreTarget: restoreTarget,
1224
+ fromVersion: fromVersion,
1225
+ toVersion: toVersion,
1086
1226
  });
1087
1227
  }
1088
1228
  const event = {
1089
1229
  id: `restore-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
1090
- restoreTarget: input.restoreTarget,
1091
- fromVersion: input.fromVersion,
1092
- toVersion: input.toVersion,
1230
+ restoreTarget: restoreTarget,
1231
+ fromVersion: fromVersion,
1232
+ toVersion: toVersion,
1093
1233
  triggeredBy: input?.triggeredBy ?? "operator",
1094
1234
  reason: typeof input?.reason === "string" ? input.reason : "manual_restore",
1095
1235
  completedEntities: restoreResult.completedEntities,
@@ -1241,7 +1381,10 @@ export function createOpsRouter(deps) {
1241
1381
  capabilityIntent,
1242
1382
  platformId,
1243
1383
  });
1244
- const atmosphere = getBaselineAtmosphereTemplate();
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);
1245
1388
  const envelope = {
1246
1389
  ok: true,
1247
1390
  command: "guidance_payload",
@@ -1258,6 +1401,8 @@ export function createOpsRouter(deps) {
1258
1401
  impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
1259
1402
  atmosphereText: atmosphere.text,
1260
1403
  atmosphereReviewStatus: atmosphere.reviewStatus,
1404
+ expressionBoundaryConstraints: expressionBoundary.constraints,
1405
+ expressionBoundaryStyle: expressionBoundary.style,
1261
1406
  },
1262
1407
  warnings: impulseResult.source === "none"
1263
1408
  ? ["no_impulse_available_for_this_scene_and_capability"]
@@ -1266,6 +1411,7 @@ export function createOpsRouter(deps) {
1266
1411
  "guidance/capability-class.ts",
1267
1412
  "guidance/impulse-assembler.ts",
1268
1413
  "guidance/template-registry.ts",
1414
+ "guidance/output-guard.ts",
1269
1415
  ],
1270
1416
  };
1271
1417
  return envelope;