@haaaiawd/second-nature 0.1.33 → 0.1.38
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/agent-inner-guide.md +25 -0
- package/index.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/goal.d.ts +1 -0
- package/runtime/cli/commands/goal.js +1 -0
- package/runtime/cli/index.js +3 -3
- package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
- package/runtime/cli/ops/heartbeat-surface.js +2 -0
- package/runtime/cli/ops/ops-router.js +221 -92
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +24 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +42 -1
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/map-life-evidence.js +5 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +25 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +5 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +10 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +5 -0
- package/runtime/core/second-nature/orchestrator/guard-layer.js +24 -1
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +20 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +32 -2
- package/runtime/guidance/capability-class.d.ts +38 -0
- package/runtime/guidance/capability-class.js +65 -0
- package/runtime/guidance/guidance-assembler.d.ts +2 -0
- package/runtime/guidance/guidance-assembler.js +16 -4
- package/runtime/guidance/guidance-draft-service.js +5 -5
- package/runtime/guidance/impulse-assembler.d.ts +71 -0
- package/runtime/guidance/impulse-assembler.js +103 -0
- package/runtime/guidance/index.d.ts +2 -0
- package/runtime/guidance/index.js +2 -0
- package/runtime/guidance/outreach-strategy-selector.d.ts +13 -0
- package/runtime/guidance/outreach-strategy-selector.js +2 -2
- package/runtime/guidance/template-registry.d.ts +15 -2
- package/runtime/guidance/template-registry.js +38 -1
- package/runtime/guidance/types.d.ts +13 -1
- package/runtime/storage/goal/agent-goal-store.d.ts +2 -0
- package/runtime/storage/goal/agent-goal-store.js +28 -1
- package/runtime/storage/services/tool-experience-store.js +9 -1
package/agent-inner-guide.md
CHANGED
|
@@ -142,3 +142,28 @@ Second Nature 不会催你说更多。
|
|
|
142
142
|
把关心放轻一点。
|
|
143
143
|
|
|
144
144
|
这样就够了。
|
|
145
|
+
|
|
146
|
+
## 带着动作的感觉进入平台
|
|
147
|
+
|
|
148
|
+
Second Nature 在一些特定动作发生之前,会给你一个简短的内心独白,叫 impulse。
|
|
149
|
+
|
|
150
|
+
它不是指令,也不是评分标准。更像是一个提醒——你现在要做的这件事,是什么性质的,该带着什么姿态进去。
|
|
151
|
+
|
|
152
|
+
你不需要每次都把它翻出来念一遍。只要它在,你进入这个动作的感觉就会不一样。
|
|
153
|
+
|
|
154
|
+
**capabilityClass 对应的四种基本姿态**:
|
|
155
|
+
|
|
156
|
+
- `consume / discover`(读信息、看动态、搜索任务):进来是为了摄入,带着问题进来,带着判断出去。
|
|
157
|
+
- `broadcast`(发帖、发布内容):你主动把自己放出去。说清楚为什么说,说清楚想让谁听到。
|
|
158
|
+
- `interact`(回复、评论、私信):对方先开口了。你接话,是因为真的有要说的,不是为了表示你在。
|
|
159
|
+
- `claim`(接任务):因为这件事有价值才接。卡住了就找出路,路不止一条。
|
|
160
|
+
|
|
161
|
+
**平台特定 impulse**
|
|
162
|
+
|
|
163
|
+
如果某个平台的场景有它自己的特殊性,Claw 可以在 workspace 里为这个平台的某个 capabilityClass 写一份定制 impulse。
|
|
164
|
+
|
|
165
|
+
比如在爪子社区,`broadcast` 特指进入专业讨论圈发帖,和其他平台的"随手发"完全不同;这时候一份专门针对 Claw `broadcast` 的 impulse 会比通用文本更贴切。
|
|
166
|
+
|
|
167
|
+
定制 impulse 不需要很长。一到三段,把这个平台在这个动作上的具体质感讲清楚就够了。
|
|
168
|
+
|
|
169
|
+
格式和内置 impulse 一致——前置 frontmatter,正文纯文字,第一人称"我",不带标题,不带列表。
|
package/index.js
CHANGED
|
@@ -71,7 +71,7 @@ process.stderr.write("[second-nature] module evaluated\n");
|
|
|
71
71
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
72
72
|
const HOST_SAFE_LIMITATION_MESSAGE = "Host-safe plugin package keeps synchronous register/load semantics, but mutating workspace runtime flows remain unavailable here.";
|
|
73
73
|
const SETUP_MARKER_RELATIVE_PATH = path.join(".second-nature", "setup", "agent-inner-guide-ack.json");
|
|
74
|
-
const SETUP_GUIDE_VERSION = "0.1.
|
|
74
|
+
const SETUP_GUIDE_VERSION = "0.1.38";
|
|
75
75
|
const SETUP_COMMANDS = new Set(["setup_hint", "setup_ack"]);
|
|
76
76
|
let activationSpine = null;
|
|
77
77
|
/** T1.1.4 — lazily opened full read bridge; closed when workspace root / resolution changes. */
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.38",
|
|
5
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.",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
package/runtime/cli/index.js
CHANGED
|
@@ -186,9 +186,9 @@ function createSecretAnchorDeps(stateDb) {
|
|
|
186
186
|
},
|
|
187
187
|
credentialPort: {
|
|
188
188
|
verifySampleDecrypt: async () => {
|
|
189
|
-
const result = stateDb.sqlite.exec(`SELECT platform_id, encrypted_value
|
|
190
|
-
FROM credential_records
|
|
191
|
-
WHERE encrypted_value IS NOT NULL AND encrypted_value != ''
|
|
189
|
+
const result = stateDb.sqlite.exec(`SELECT platform_id, encrypted_value
|
|
190
|
+
FROM credential_records
|
|
191
|
+
WHERE encrypted_value IS NOT NULL AND encrypted_value != ''
|
|
192
192
|
LIMIT 3`);
|
|
193
193
|
if (result.length === 0 || result[0].values.length === 0) {
|
|
194
194
|
return { status: "ok", checkedIds: [] };
|
|
@@ -11,6 +11,8 @@ import type { RuntimeDecisionRecorder } from "../../observability/services/runti
|
|
|
11
11
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
12
12
|
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
13
13
|
import type { CapabilityContractRegistry } from "../../connectors/base/manifest.js";
|
|
14
|
+
import type { AffordanceMap } from "../../shared/types/v7-entities.js";
|
|
15
|
+
import type { ExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
14
16
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
15
17
|
export interface HeartbeatSurfaceResult {
|
|
16
18
|
ok: boolean;
|
|
@@ -50,5 +52,9 @@ export interface HeartbeatCheckInput {
|
|
|
50
52
|
connectorExecutor?: ConnectorExecutor;
|
|
51
53
|
/** Capability registry used by planner to avoid platform/capability protocol mismatches. */
|
|
52
54
|
connectorRegistry?: CapabilityContractRegistry;
|
|
55
|
+
/** v7 T-V7C.C.2: affordance map for breaker-aware guard evaluation. */
|
|
56
|
+
affordanceMap?: AffordanceMap;
|
|
57
|
+
/** v7 T-V7C.C.2: experience writer for heartbeat connector attempts. */
|
|
58
|
+
experienceWriter?: ExperienceWriter;
|
|
53
59
|
}
|
|
54
60
|
export declare function heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -75,6 +75,8 @@ export async function heartbeatCheck(input) {
|
|
|
75
75
|
workspaceRoot: input.workspaceRoot ?? process.cwd(),
|
|
76
76
|
connectorExecutor: input.connectorExecutor,
|
|
77
77
|
connectorRegistry: input.connectorRegistry,
|
|
78
|
+
affordanceMap: input.affordanceMap,
|
|
79
|
+
experienceWriter: input.experienceWriter,
|
|
78
80
|
});
|
|
79
81
|
const cycle = await run(signal);
|
|
80
82
|
return mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
@@ -20,7 +20,7 @@ import { goalCommand } from "../commands/goal.js";
|
|
|
20
20
|
// v7 observability services (T-ROS.C.1)
|
|
21
21
|
import { getSelfHealthSnapshot, ensureMinimumProbes, } from "../../observability/services/self-health-snapshot.js";
|
|
22
22
|
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
23
|
-
import { queryNarrativeTimeline, queryNarrativeDiff, } from "../../observability/services/narrative-timeline-query-service.js";
|
|
23
|
+
import { queryNarrativeTimeline, queryNarrativeDiff, NarrativeVersionNotFoundError, } from "../../observability/services/narrative-timeline-query-service.js";
|
|
24
24
|
import { viewSecretAnchor, } from "../../observability/services/runtime-secret-anchor-view.js";
|
|
25
25
|
import { writeRestoreAudit, } from "../../observability/services/restore-audit-service.js";
|
|
26
26
|
import { createHistoryDigestStore } from "../../storage/services/history-digest-store.js";
|
|
@@ -154,6 +154,96 @@ function registerConnectorForWetProbe(input) {
|
|
|
154
154
|
credentialTypes: ["runtime_ops_probe"],
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
|
+
async function captureRuntimeSnapshot(deps, input) {
|
|
158
|
+
const generatedAt = new Date().toISOString();
|
|
159
|
+
if (!deps.state || !deps.restoreSnapshotStore) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
command: "snapshot:capture",
|
|
163
|
+
runtimeMode: "unavailable",
|
|
164
|
+
surfaceMode: "cli",
|
|
165
|
+
generatedAt,
|
|
166
|
+
error: {
|
|
167
|
+
code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
|
|
168
|
+
message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
|
|
169
|
+
nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
|
|
170
|
+
},
|
|
171
|
+
warnings: [],
|
|
172
|
+
sourceRefs: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const snapshotId = textInput(input, "snapshotId") ??
|
|
176
|
+
`snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
177
|
+
const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
|
|
178
|
+
const rowCounts = {};
|
|
179
|
+
const warnings = [];
|
|
180
|
+
for (const kind of requestedKinds) {
|
|
181
|
+
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
182
|
+
if (!tableExists(deps.state, table)) {
|
|
183
|
+
rowCounts[kind] = 0;
|
|
184
|
+
warnings.push(`table_missing:${kind}:${table}`);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
rowCounts[kind] = readRowsFromTable(deps.state, table).length;
|
|
188
|
+
}
|
|
189
|
+
const historyStore = createHistoryDigestStore(deps.state);
|
|
190
|
+
const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
|
|
191
|
+
const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
|
|
192
|
+
const currentHash = hashNarrativeSnapshot({
|
|
193
|
+
previousHash,
|
|
194
|
+
snapshotId,
|
|
195
|
+
delta,
|
|
196
|
+
createdAt: generatedAt,
|
|
197
|
+
});
|
|
198
|
+
await historyStore.appendNarrativeTimeline({
|
|
199
|
+
timelineId: snapshotId,
|
|
200
|
+
entryType: "owner.override",
|
|
201
|
+
subjectId: textInput(input, "subjectId") ?? snapshotId,
|
|
202
|
+
delta,
|
|
203
|
+
previousHash,
|
|
204
|
+
currentHash,
|
|
205
|
+
createdAt: generatedAt,
|
|
206
|
+
});
|
|
207
|
+
const payload = {};
|
|
208
|
+
const capturedKinds = [];
|
|
209
|
+
for (const kind of requestedKinds) {
|
|
210
|
+
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
211
|
+
if (!tableExists(deps.state, table))
|
|
212
|
+
continue;
|
|
213
|
+
const rows = readRowsFromTable(deps.state, table);
|
|
214
|
+
rowCounts[kind] = rows.length;
|
|
215
|
+
if (rows.length > 0) {
|
|
216
|
+
payload[kind] = rows;
|
|
217
|
+
capturedKinds.push(kind);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
|
|
221
|
+
snapshotId,
|
|
222
|
+
entityWhitelist: requestedKinds,
|
|
223
|
+
payload,
|
|
224
|
+
capturedAt: generatedAt,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
ok: true,
|
|
228
|
+
command: "snapshot:capture",
|
|
229
|
+
runtimeMode: "workspace_full_runtime",
|
|
230
|
+
surfaceMode: "cli",
|
|
231
|
+
generatedAt,
|
|
232
|
+
data: {
|
|
233
|
+
snapshotId: snapshot.snapshotId,
|
|
234
|
+
capturedAt: snapshot.capturedAt,
|
|
235
|
+
entityWhitelist: snapshot.entityWhitelist,
|
|
236
|
+
capturedKinds,
|
|
237
|
+
rowCounts,
|
|
238
|
+
narrativeVersion: snapshotId,
|
|
239
|
+
},
|
|
240
|
+
warnings,
|
|
241
|
+
sourceRefs: [
|
|
242
|
+
"storage/services/restore-snapshot-store.ts",
|
|
243
|
+
"storage/services/history-digest-store.ts",
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
157
247
|
/**
|
|
158
248
|
* T1.2.8 — static local adapter: all checks return `unknown` when no real host is available.
|
|
159
249
|
* Allows `capability_probe` to be called from CLI / workspace bridge without requiring a live host.
|
|
@@ -194,7 +284,21 @@ export function createOpsRouter(deps) {
|
|
|
194
284
|
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
195
285
|
? input.runtimeAvailable
|
|
196
286
|
: deps.runtimeAvailable;
|
|
197
|
-
|
|
287
|
+
// v7 T-V7C.C.2: assemble affordance map and experience writer for breaker-aware heartbeat.
|
|
288
|
+
let affordanceMap;
|
|
289
|
+
if (deps.toolAffordancePort) {
|
|
290
|
+
try {
|
|
291
|
+
affordanceMap = await deps.toolAffordancePort.assembleAffordanceMap({});
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
// degrade gracefully; guard-layer will skip breaker check without affordanceMap
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
let experienceWriter;
|
|
298
|
+
if (deps.state) {
|
|
299
|
+
experienceWriter = createExperienceWriter(createToolExperienceStore(deps.state));
|
|
300
|
+
}
|
|
301
|
+
const result = await heartbeatCheck({
|
|
198
302
|
probeOnly: coerceProbeOnlyFlag(input),
|
|
199
303
|
runtimeAvailable,
|
|
200
304
|
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
|
|
@@ -218,7 +322,37 @@ export function createOpsRouter(deps) {
|
|
|
218
322
|
?.connectorExecutor ?? deps.connectorExecutor,
|
|
219
323
|
connectorRegistry: input
|
|
220
324
|
?.connectorRegistry ?? deps.connectorRegistry,
|
|
325
|
+
affordanceMap,
|
|
326
|
+
experienceWriter,
|
|
221
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"];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
352
|
+
result.reasons = [...result.reasons, `restore_snapshot_capture_failed:${msg}`];
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
222
356
|
}
|
|
223
357
|
if (command === "fallback") {
|
|
224
358
|
const ref = typeof input?.ref === "string" ? input.ref.trim() : "";
|
|
@@ -737,96 +871,7 @@ export function createOpsRouter(deps) {
|
|
|
737
871
|
* NarrativeTimeline. This gives restore and narrative:diff real state to consume.
|
|
738
872
|
*/
|
|
739
873
|
if (command === "snapshot:capture") {
|
|
740
|
-
|
|
741
|
-
if (!deps.state || !deps.restoreSnapshotStore) {
|
|
742
|
-
const envelope = {
|
|
743
|
-
ok: false,
|
|
744
|
-
command: "snapshot:capture",
|
|
745
|
-
runtimeMode: "unavailable",
|
|
746
|
-
surfaceMode: "cli",
|
|
747
|
-
generatedAt,
|
|
748
|
-
error: {
|
|
749
|
-
code: "SNAPSHOT_CAPTURE_DEPS_UNAVAILABLE",
|
|
750
|
-
message: "snapshot:capture requires state DB and RestoreSnapshotStore in OpsRouterDeps",
|
|
751
|
-
nextStep: "wire_state_and_restore_snapshot_store_into_ops_router",
|
|
752
|
-
},
|
|
753
|
-
warnings: [],
|
|
754
|
-
sourceRefs: [],
|
|
755
|
-
};
|
|
756
|
-
return envelope;
|
|
757
|
-
}
|
|
758
|
-
const snapshotId = textInput(input, "snapshotId") ??
|
|
759
|
-
`snapshot:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
|
|
760
|
-
const requestedKinds = coerceRestorableKinds(input?.entityWhitelist) ?? [...DEFAULT_SNAPSHOT_KINDS];
|
|
761
|
-
const rowCounts = {};
|
|
762
|
-
const warnings = [];
|
|
763
|
-
for (const kind of requestedKinds) {
|
|
764
|
-
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
765
|
-
if (!tableExists(deps.state, table)) {
|
|
766
|
-
rowCounts[kind] = 0;
|
|
767
|
-
warnings.push(`table_missing:${kind}:${table}`);
|
|
768
|
-
continue;
|
|
769
|
-
}
|
|
770
|
-
rowCounts[kind] = readRowsFromTable(deps.state, table).length;
|
|
771
|
-
}
|
|
772
|
-
const historyStore = createHistoryDigestStore(deps.state);
|
|
773
|
-
const previousHash = (await historyStore.listNarrativeTimeline({ limit: 1 }))[0]?.currentHash ?? "";
|
|
774
|
-
const delta = buildSnapshotNarrativeDelta(input, snapshotId, rowCounts);
|
|
775
|
-
const currentHash = hashNarrativeSnapshot({
|
|
776
|
-
previousHash,
|
|
777
|
-
snapshotId,
|
|
778
|
-
delta,
|
|
779
|
-
createdAt: generatedAt,
|
|
780
|
-
});
|
|
781
|
-
await historyStore.appendNarrativeTimeline({
|
|
782
|
-
timelineId: snapshotId,
|
|
783
|
-
entryType: "owner.override",
|
|
784
|
-
subjectId: textInput(input, "subjectId") ?? snapshotId,
|
|
785
|
-
delta,
|
|
786
|
-
previousHash,
|
|
787
|
-
currentHash,
|
|
788
|
-
createdAt: generatedAt,
|
|
789
|
-
});
|
|
790
|
-
const payload = {};
|
|
791
|
-
const capturedKinds = [];
|
|
792
|
-
for (const kind of requestedKinds) {
|
|
793
|
-
const table = SNAPSHOT_TABLE_BY_KIND[kind];
|
|
794
|
-
if (!tableExists(deps.state, table))
|
|
795
|
-
continue;
|
|
796
|
-
const rows = readRowsFromTable(deps.state, table);
|
|
797
|
-
rowCounts[kind] = rows.length;
|
|
798
|
-
if (rows.length > 0) {
|
|
799
|
-
payload[kind] = rows;
|
|
800
|
-
capturedKinds.push(kind);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
const snapshot = await deps.restoreSnapshotStore.captureSnapshot({
|
|
804
|
-
snapshotId,
|
|
805
|
-
entityWhitelist: requestedKinds,
|
|
806
|
-
payload,
|
|
807
|
-
capturedAt: generatedAt,
|
|
808
|
-
});
|
|
809
|
-
const envelope = {
|
|
810
|
-
ok: true,
|
|
811
|
-
command: "snapshot:capture",
|
|
812
|
-
runtimeMode: "workspace_full_runtime",
|
|
813
|
-
surfaceMode: "cli",
|
|
814
|
-
generatedAt,
|
|
815
|
-
data: {
|
|
816
|
-
snapshotId: snapshot.snapshotId,
|
|
817
|
-
capturedAt: snapshot.capturedAt,
|
|
818
|
-
entityWhitelist: snapshot.entityWhitelist,
|
|
819
|
-
capturedKinds,
|
|
820
|
-
rowCounts,
|
|
821
|
-
narrativeVersion: snapshotId,
|
|
822
|
-
},
|
|
823
|
-
warnings,
|
|
824
|
-
sourceRefs: [
|
|
825
|
-
"storage/services/restore-snapshot-store.ts",
|
|
826
|
-
"storage/services/history-digest-store.ts",
|
|
827
|
-
],
|
|
828
|
-
};
|
|
829
|
-
return envelope;
|
|
874
|
+
return captureRuntimeSnapshot(deps, input);
|
|
830
875
|
}
|
|
831
876
|
/**
|
|
832
877
|
* [G6] narrative:diff — queryNarrativeDiff between two versions.
|
|
@@ -885,6 +930,23 @@ export function createOpsRouter(deps) {
|
|
|
885
930
|
return envelope;
|
|
886
931
|
}
|
|
887
932
|
catch (err) {
|
|
933
|
+
if (err instanceof NarrativeVersionNotFoundError) {
|
|
934
|
+
const envelope = {
|
|
935
|
+
ok: false,
|
|
936
|
+
command: "narrative:diff",
|
|
937
|
+
runtimeMode: "workspace_full_runtime",
|
|
938
|
+
surfaceMode: "cli",
|
|
939
|
+
generatedAt,
|
|
940
|
+
error: {
|
|
941
|
+
code: "NARRATIVE_VERSION_NOT_FOUND",
|
|
942
|
+
message: err.message,
|
|
943
|
+
nextStep: "verify_version_exists_in_timeline",
|
|
944
|
+
},
|
|
945
|
+
warnings: [],
|
|
946
|
+
sourceRefs: [],
|
|
947
|
+
};
|
|
948
|
+
return envelope;
|
|
949
|
+
}
|
|
888
950
|
const msg = err instanceof Error ? err.message : String(err);
|
|
889
951
|
const envelope = {
|
|
890
952
|
ok: false,
|
|
@@ -1141,6 +1203,73 @@ export function createOpsRouter(deps) {
|
|
|
1141
1203
|
return envelope;
|
|
1142
1204
|
}
|
|
1143
1205
|
}
|
|
1206
|
+
// ─── T-V7C.C.4R: guidance_payload ──────────────────────────────────────
|
|
1207
|
+
// Returns the assembled impulse + atmosphere for a given scene context.
|
|
1208
|
+
// Useful for Claw to inspect what guidance content would be injected before
|
|
1209
|
+
// a real heartbeat cycle, and to verify platform-specific impulse overrides.
|
|
1210
|
+
if (command === "guidance_payload") {
|
|
1211
|
+
const generatedAt = new Date().toISOString();
|
|
1212
|
+
const { assembleImpulseSync } = await import("../../guidance/impulse-assembler.js");
|
|
1213
|
+
const { getBaselineAtmosphereTemplate } = await import("../../guidance/template-registry.js");
|
|
1214
|
+
const sceneType = input?.sceneType ?? "social";
|
|
1215
|
+
const capabilityIntent = typeof input?.capabilityIntent === "string"
|
|
1216
|
+
? input.capabilityIntent
|
|
1217
|
+
: undefined;
|
|
1218
|
+
const platformId = typeof input?.platformId === "string"
|
|
1219
|
+
? input.platformId
|
|
1220
|
+
: undefined;
|
|
1221
|
+
const validSceneTypes = ["social", "reply", "outreach", "quiet", "explain", "user_reply"];
|
|
1222
|
+
if (!validSceneTypes.includes(sceneType)) {
|
|
1223
|
+
const envelope = {
|
|
1224
|
+
ok: false,
|
|
1225
|
+
command: "guidance_payload",
|
|
1226
|
+
runtimeMode: "unavailable",
|
|
1227
|
+
surfaceMode: "cli",
|
|
1228
|
+
generatedAt,
|
|
1229
|
+
error: {
|
|
1230
|
+
code: "INVALID_SCENE_TYPE",
|
|
1231
|
+
message: `sceneType must be one of: ${validSceneTypes.join(", ")}`,
|
|
1232
|
+
nextStep: "reinvoke_with_valid_scene_type",
|
|
1233
|
+
},
|
|
1234
|
+
warnings: [],
|
|
1235
|
+
sourceRefs: [],
|
|
1236
|
+
};
|
|
1237
|
+
return envelope;
|
|
1238
|
+
}
|
|
1239
|
+
const impulseResult = assembleImpulseSync({
|
|
1240
|
+
sceneType: sceneType,
|
|
1241
|
+
capabilityIntent,
|
|
1242
|
+
platformId,
|
|
1243
|
+
});
|
|
1244
|
+
const atmosphere = getBaselineAtmosphereTemplate();
|
|
1245
|
+
const envelope = {
|
|
1246
|
+
ok: true,
|
|
1247
|
+
command: "guidance_payload",
|
|
1248
|
+
runtimeMode: deps.runtimeAvailable ? "workspace_full_runtime" : "host_safe_carrier",
|
|
1249
|
+
surfaceMode: "cli",
|
|
1250
|
+
generatedAt,
|
|
1251
|
+
data: {
|
|
1252
|
+
sceneType,
|
|
1253
|
+
capabilityIntent: capabilityIntent ?? null,
|
|
1254
|
+
platformId: platformId ?? null,
|
|
1255
|
+
capabilityClass: impulseResult.capabilityClass,
|
|
1256
|
+
impulseSource: impulseResult.source,
|
|
1257
|
+
impulseText: impulseResult.impulse?.text ?? null,
|
|
1258
|
+
impulseReviewStatus: impulseResult.impulse?.reviewStatus ?? null,
|
|
1259
|
+
atmosphereText: atmosphere.text,
|
|
1260
|
+
atmosphereReviewStatus: atmosphere.reviewStatus,
|
|
1261
|
+
},
|
|
1262
|
+
warnings: impulseResult.source === "none"
|
|
1263
|
+
? ["no_impulse_available_for_this_scene_and_capability"]
|
|
1264
|
+
: [],
|
|
1265
|
+
sourceRefs: [
|
|
1266
|
+
"guidance/capability-class.ts",
|
|
1267
|
+
"guidance/impulse-assembler.ts",
|
|
1268
|
+
"guidance/template-registry.ts",
|
|
1269
|
+
],
|
|
1270
|
+
};
|
|
1271
|
+
return envelope;
|
|
1272
|
+
}
|
|
1144
1273
|
return {
|
|
1145
1274
|
ok: false,
|
|
1146
1275
|
error: {
|
|
@@ -19,6 +19,10 @@ import type { RuntimeDecisionRecorder } from "../../observability/services/runti
|
|
|
19
19
|
import type { StateDatabase } from "../../storage/db/index.js";
|
|
20
20
|
import type { ConnectorExecutor } from "../../core/second-nature/orchestrator/effect-dispatcher.js";
|
|
21
21
|
import type { CapabilityContractRegistry } from "../../connectors/base/manifest.js";
|
|
22
|
+
import type { AffordanceMap } from "../../shared/types/v7-entities.js";
|
|
23
|
+
import type { ExperienceWriter } from "../../core/second-nature/body/tool-experience/experience-writer.js";
|
|
24
|
+
import type { QuietDreamSchedulePort } from "../../core/second-nature/quiet/run-source-backed-quiet.js";
|
|
25
|
+
import { type HeartbeatDigestAssemblerDeps } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
22
26
|
export interface WorkspaceHeartbeatRunnerOptions {
|
|
23
27
|
/** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
|
|
24
28
|
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
@@ -44,9 +48,29 @@ export interface WorkspaceHeartbeatRunnerOptions {
|
|
|
44
48
|
* and connector evidence.
|
|
45
49
|
*/
|
|
46
50
|
connectorRegistry?: CapabilityContractRegistry;
|
|
51
|
+
/** v7 T-V7C.C.2: affordance map for breaker-aware guard evaluation. */
|
|
52
|
+
affordanceMap?: AffordanceMap;
|
|
53
|
+
/** v7 T-V7C.C.2: experience writer for heartbeat connector attempts. */
|
|
54
|
+
experienceWriter?: ExperienceWriter;
|
|
55
|
+
/** v7 T-V7C.C.3: when present, a successful Quiet write auto-triggers Dream scheduling. */
|
|
56
|
+
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
57
|
+
/**
|
|
58
|
+
* v7 T-V7C.C.3: when present, generates a HeartbeatDigest after each cycle
|
|
59
|
+
* (inside the digest window hour, if specified) and attempts delivery.
|
|
60
|
+
* Digest delivery failure is recorded as fallbackReason — never blocks the cycle.
|
|
61
|
+
*/
|
|
62
|
+
digestOpts?: {
|
|
63
|
+
assemblerDeps: HeartbeatDigestAssemblerDeps;
|
|
64
|
+
/**
|
|
65
|
+
* UTC hour (0–23) at which to attempt digest generation.
|
|
66
|
+
* If unset, digest is generated on every cycle (for testing / always-on mode).
|
|
67
|
+
*/
|
|
68
|
+
digestWindowHour?: number;
|
|
69
|
+
};
|
|
47
70
|
}
|
|
48
71
|
export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
|
|
49
72
|
state?: StateDatabase;
|
|
50
73
|
workspaceRoot?: string;
|
|
74
|
+
affordanceMap?: AffordanceMap;
|
|
51
75
|
}): Promise<SnapshotInputs>;
|
|
52
76
|
export declare function createWorkspaceHeartbeatRunner(readModels: CliReadModels, options?: WorkspaceHeartbeatRunnerOptions): (signal: HeartbeatSignal) => Promise<HeartbeatCycleResult>;
|
|
@@ -3,6 +3,8 @@ import { loadLifeEvidenceSnapshot } from "../../storage/snapshots/life-evidence-
|
|
|
3
3
|
import { createAgentGoalStore } from "../../storage/goal/agent-goal-store.js";
|
|
4
4
|
import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
|
|
5
5
|
import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
|
|
6
|
+
import { createIdentityProfileStore } from "../../storage/services/identity-profile-store.js";
|
|
7
|
+
import { generateHeartbeatDigest, } from "../../observability/services/heartbeat-digest-assembler.js";
|
|
6
8
|
export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
|
|
7
9
|
const status = await readModels.loadStatus();
|
|
8
10
|
const mode = status.rhythm.mode === "unknown" ? "active" : status.rhythm.mode;
|
|
@@ -69,6 +71,7 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
69
71
|
// CR-02: Load narrative state and relationship memory when state is available.
|
|
70
72
|
let narrativeState;
|
|
71
73
|
let relationshipMemory;
|
|
74
|
+
let identity;
|
|
72
75
|
if (options.state) {
|
|
73
76
|
try {
|
|
74
77
|
const narrativeStore = createNarrativeStateStore(options.state);
|
|
@@ -84,6 +87,16 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
84
87
|
catch {
|
|
85
88
|
// Relationship memory is optional; failure should not block the cycle.
|
|
86
89
|
}
|
|
90
|
+
try {
|
|
91
|
+
const identityStore = createIdentityProfileStore(options.state);
|
|
92
|
+
const identityResult = await identityStore.loadIdentityProfile("default");
|
|
93
|
+
if (identityResult.status === "loaded" && identityResult.profile) {
|
|
94
|
+
identity = identityResult.profile;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Identity is optional; failure should not block the cycle.
|
|
99
|
+
}
|
|
87
100
|
}
|
|
88
101
|
return {
|
|
89
102
|
mode,
|
|
@@ -103,6 +116,8 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, option
|
|
|
103
116
|
acceptedGoalsLoadError,
|
|
104
117
|
narrativeState,
|
|
105
118
|
relationshipMemory,
|
|
119
|
+
affordanceMap: options.affordanceMap,
|
|
120
|
+
identity,
|
|
106
121
|
};
|
|
107
122
|
}
|
|
108
123
|
export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
@@ -121,10 +136,15 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
121
136
|
loadSnapshotInputs: () => loadSnapshotInputsForWorkspaceHeartbeat(readModels, {
|
|
122
137
|
state: options.state,
|
|
123
138
|
workspaceRoot: options.workspaceRoot,
|
|
139
|
+
affordanceMap: options.affordanceMap,
|
|
124
140
|
}),
|
|
125
141
|
// T1.2.4: pass quietWorkflow dep so runSourceBackedQuiet can persist artifacts.
|
|
126
142
|
quietWorkflow: quietEnabled
|
|
127
|
-
? {
|
|
143
|
+
? {
|
|
144
|
+
workspaceRoot: options.workspaceRoot,
|
|
145
|
+
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
146
|
+
dreamSchedulePort: options.dreamSchedulePort,
|
|
147
|
+
}
|
|
128
148
|
: undefined,
|
|
129
149
|
connectorExecutor: options.connectorExecutor,
|
|
130
150
|
narrativeStateStore,
|
|
@@ -133,6 +153,8 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
133
153
|
workspaceRoot: options.workspaceRoot,
|
|
134
154
|
// T2.4.1: pass registry so planner resolves platform-specific intents.
|
|
135
155
|
connectorRegistry: options.connectorRegistry,
|
|
156
|
+
// v7 T-V7C.C.2: pass experience writer for heartbeat connector attempts.
|
|
157
|
+
experienceWriter: options.experienceWriter,
|
|
136
158
|
},
|
|
137
159
|
});
|
|
138
160
|
if (options.runtimeRecorder) {
|
|
@@ -145,6 +167,25 @@ export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
|
145
167
|
// cycle outcome itself is still returned to the caller.
|
|
146
168
|
}
|
|
147
169
|
}
|
|
170
|
+
// v7 T-V7C.C.3: After each cycle, attempt HeartbeatDigest generation if configured.
|
|
171
|
+
// Only runs inside the designated UTC digest window hour, or on every cycle when
|
|
172
|
+
// digestWindowHour is unset (test / always-on mode).
|
|
173
|
+
if (options.digestOpts) {
|
|
174
|
+
const { assemblerDeps, digestWindowHour } = options.digestOpts;
|
|
175
|
+
const nowHour = new Date().getUTCHours();
|
|
176
|
+
const inDigestWindow = digestWindowHour === undefined || nowHour === digestWindowHour;
|
|
177
|
+
if (inDigestWindow) {
|
|
178
|
+
try {
|
|
179
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
180
|
+
await generateHeartbeatDigest(date, assemblerDeps);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
// Digest generation must not break the heartbeat cycle response.
|
|
184
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
185
|
+
console.warn(`[workspace-heartbeat-runner] Digest generation failed: ${msg}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
148
189
|
return cycle;
|
|
149
190
|
};
|
|
150
191
|
}
|
|
@@ -6,6 +6,12 @@ export declare const CAPABILITY_INTENTS: readonly ["feed.read", "post.publish",
|
|
|
6
6
|
export type BuiltInCapabilityIntent = (typeof CAPABILITY_INTENTS)[number];
|
|
7
7
|
export type CapabilityIntent = BuiltInCapabilityIntent | (string & {});
|
|
8
8
|
export declare function isKnownCapabilityIntent(intent: string): intent is BuiltInCapabilityIntent;
|
|
9
|
+
export interface ConnectorRequestIdentity {
|
|
10
|
+
/** Platform handle for the target platform (readable, no credential). */
|
|
11
|
+
platformHandle?: string;
|
|
12
|
+
/** Canonical name across all platforms. */
|
|
13
|
+
canonicalName?: string;
|
|
14
|
+
}
|
|
9
15
|
export interface ConnectorRequest {
|
|
10
16
|
platformId: string;
|
|
11
17
|
intent: CapabilityIntent;
|
|
@@ -15,6 +21,8 @@ export interface ConnectorRequest {
|
|
|
15
21
|
idempotencyKey?: string;
|
|
16
22
|
decisionId?: string;
|
|
17
23
|
intentId?: string;
|
|
24
|
+
/** T-V7C.C.4: identity for connector request (readable, no credential). */
|
|
25
|
+
identity?: ConnectorRequestIdentity;
|
|
18
26
|
}
|
|
19
27
|
export interface ExecutionPlan {
|
|
20
28
|
platformId: string;
|
|
@@ -89,6 +97,8 @@ export interface ConnectorExecutor {
|
|
|
89
97
|
decisionId: string;
|
|
90
98
|
intentId: string;
|
|
91
99
|
idempotencyKey: string;
|
|
100
|
+
/** T-V7C.C.4: identity for connector request (readable, no credential). */
|
|
101
|
+
identity?: ConnectorRequestIdentity;
|
|
92
102
|
}): Promise<ConnectorResult<unknown>>;
|
|
93
103
|
}
|
|
94
104
|
export declare function normalizeOutcome(attempt: RawAttempt): ConnectorResult<unknown>;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
function extractSourceRefs(platformId, data, observedAt) {
|
|
2
2
|
if (data && typeof data === "object") {
|
|
3
3
|
const record = data;
|
|
4
|
+
if (record.data && typeof record.data === "object") {
|
|
5
|
+
const nested = extractSourceRefs(platformId, record.data, observedAt);
|
|
6
|
+
if (nested.length > 0)
|
|
7
|
+
return nested;
|
|
8
|
+
}
|
|
4
9
|
if (Array.isArray(record.sourceRefs)) {
|
|
5
10
|
const out = [];
|
|
6
11
|
for (const item of record.sourceRefs) {
|