@haaaiawd/second-nature 0.1.34 → 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 +101 -1
- 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/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 +11 -11
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";
|
|
@@ -284,6 +284,20 @@ export function createOpsRouter(deps) {
|
|
|
284
284
|
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
285
285
|
? input.runtimeAvailable
|
|
286
286
|
: deps.runtimeAvailable;
|
|
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
|
+
}
|
|
287
301
|
const result = await heartbeatCheck({
|
|
288
302
|
probeOnly: coerceProbeOnlyFlag(input),
|
|
289
303
|
runtimeAvailable,
|
|
@@ -308,6 +322,8 @@ export function createOpsRouter(deps) {
|
|
|
308
322
|
?.connectorExecutor ?? deps.connectorExecutor,
|
|
309
323
|
connectorRegistry: input
|
|
310
324
|
?.connectorRegistry ?? deps.connectorRegistry,
|
|
325
|
+
affordanceMap,
|
|
326
|
+
experienceWriter,
|
|
311
327
|
});
|
|
312
328
|
if (result.ok &&
|
|
313
329
|
result.surfaceMode === "workspace_full_runtime" &&
|
|
@@ -914,6 +930,23 @@ export function createOpsRouter(deps) {
|
|
|
914
930
|
return envelope;
|
|
915
931
|
}
|
|
916
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
|
+
}
|
|
917
950
|
const msg = err instanceof Error ? err.message : String(err);
|
|
918
951
|
const envelope = {
|
|
919
952
|
ok: false,
|
|
@@ -1170,6 +1203,73 @@ export function createOpsRouter(deps) {
|
|
|
1170
1203
|
return envelope;
|
|
1171
1204
|
}
|
|
1172
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
|
+
}
|
|
1173
1273
|
return {
|
|
1174
1274
|
ok: false,
|
|
1175
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>;
|
|
@@ -23,6 +23,8 @@ import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
|
23
23
|
import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
24
24
|
import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
25
25
|
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
26
|
+
import type { ExperienceWriter } from "../body/tool-experience/experience-writer.js";
|
|
27
|
+
import type { QuietDreamSchedulePort } from "../quiet/run-source-backed-quiet.js";
|
|
26
28
|
export interface HeartbeatDecisionTracePayload {
|
|
27
29
|
scope: RuntimeScope;
|
|
28
30
|
status: HeartbeatCycleStatus;
|
|
@@ -43,12 +45,14 @@ export interface HeartbeatOutreachDispatchDeps {
|
|
|
43
45
|
/** Optional Quiet orchestration: when set, quiet/reflection allows run source-backed Quiet writer (T2.3.3). */
|
|
44
46
|
export interface HeartbeatQuietWorkflowDeps {
|
|
45
47
|
workspaceRoot: string;
|
|
48
|
+
/** v7 T-V7C.C.3: when present, a successful Quiet write auto-triggers Dream scheduling. */
|
|
49
|
+
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
49
53
|
* Exported for unit tests (CR-M1 wiring).
|
|
50
54
|
*/
|
|
51
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot">): Promise<HeartbeatCycleResult>;
|
|
55
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot" | "experienceWriter">): Promise<HeartbeatCycleResult>;
|
|
52
56
|
export interface HeartbeatDeps {
|
|
53
57
|
/** Load snapshot inputs from state-system */
|
|
54
58
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -71,6 +75,8 @@ export interface HeartbeatDeps {
|
|
|
71
75
|
workspaceRoot?: string;
|
|
72
76
|
/** T2.4.1: when present, planner resolves platform-specific intents. */
|
|
73
77
|
connectorRegistry?: CapabilityContractRegistry;
|
|
78
|
+
/** v7 T-V7C.C.2: when present, connector attempts write ToolExperience with triggerSource="heartbeat". */
|
|
79
|
+
experienceWriter?: ExperienceWriter;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
76
82
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -38,6 +38,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
38
38
|
day,
|
|
39
39
|
userInterestSnapshot: inputs.userInterestSnapshot,
|
|
40
40
|
workspaceRoot: deps.quietWorkflow.workspaceRoot,
|
|
41
|
+
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
42
|
+
dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
|
|
41
43
|
});
|
|
42
44
|
return quietRun.result;
|
|
43
45
|
}
|
|
@@ -64,6 +66,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
const decisionId = `decision:${intent.id}:${Date.now()}`;
|
|
69
|
+
// T-V7C.C.4: inject identity from EmbodiedContext into connector request (readable, no credential)
|
|
70
|
+
const platformHandle = runtime.identity?.platformHandles.find((h) => h.platformId === intent.platformId)?.handle;
|
|
67
71
|
const result = await deps.connectorExecutor.executeEffect({
|
|
68
72
|
platformId: intent.platformId,
|
|
69
73
|
intent: toCapabilityIntent(intent),
|
|
@@ -71,6 +75,12 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
71
75
|
decisionId,
|
|
72
76
|
intentId: intent.id,
|
|
73
77
|
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
78
|
+
identity: platformHandle || runtime.identity?.canonicalName
|
|
79
|
+
? {
|
|
80
|
+
platformHandle,
|
|
81
|
+
canonicalName: runtime.identity?.canonicalName,
|
|
82
|
+
}
|
|
83
|
+
: undefined,
|
|
74
84
|
});
|
|
75
85
|
// T3.3.1: on success, map connector result to life evidence and append.
|
|
76
86
|
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
@@ -96,6 +106,21 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
96
106
|
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
97
107
|
}
|
|
98
108
|
}
|
|
109
|
+
// v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
|
|
110
|
+
if (deps.experienceWriter) {
|
|
111
|
+
try {
|
|
112
|
+
await deps.experienceWriter.recordExperience({
|
|
113
|
+
connectorId: intent.platformId,
|
|
114
|
+
capabilityId: toCapabilityIntent(intent),
|
|
115
|
+
result,
|
|
116
|
+
triggerSource: "heartbeat",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
+
console.warn(`[heartbeat] ToolExperience record failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
99
124
|
const base = {
|
|
100
125
|
scope: "rhythm",
|
|
101
126
|
status: "intent_selected",
|
|
@@ -5,6 +5,7 @@ import type { ContinuitySnapshot, ControlPlaneSourceRef } from "../types.js";
|
|
|
5
5
|
import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
|
|
6
6
|
import { type PlannerRhythmWindowSlice } from "../rhythm/planner-rhythm-window.js";
|
|
7
7
|
import type { SnapshotInputs } from "./snapshot-builder.js";
|
|
8
|
+
import type { AffordanceMap } from "../../../shared/types/v7-entities.js";
|
|
8
9
|
export interface PlannerLifeEvidenceSlice {
|
|
9
10
|
evidenceRefs: ControlPlaneSourceRef[];
|
|
10
11
|
platformEventCount: number;
|
|
@@ -23,6 +24,10 @@ export interface HeartbeatRuntimeSnapshot {
|
|
|
23
24
|
hardGuards: HardGuardDeps;
|
|
24
25
|
narrativeState?: import("../../../storage/narrative/narrative-state-store.js").NarrativeState;
|
|
25
26
|
relationshipMemory?: import("../../../storage/relationship/relationship-memory-store.js").RelationshipMemory;
|
|
27
|
+
/** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
|
|
28
|
+
affordanceMap?: AffordanceMap;
|
|
29
|
+
/** T-V7C.C.4: identity profile for connector request identity injection. */
|
|
30
|
+
identity?: import("../../../shared/types/v7-entities.js").IdentityProfile;
|
|
26
31
|
}
|
|
27
32
|
export declare function buildLifeEvidenceSliceFromInputs(inputs: SnapshotInputs): PlannerLifeEvidenceSlice;
|
|
28
33
|
export declare function buildHardGuardDeps(continuity: ContinuitySnapshot, inputs: SnapshotInputs): HardGuardDeps;
|
|
@@ -31,5 +31,14 @@ export function buildHeartbeatRuntimeSnapshot(timestamp, inputs, continuity) {
|
|
|
31
31
|
const rhythmWindow = buildPlannerRhythmWindow(timestamp, continuity, policy);
|
|
32
32
|
const lifeEvidence = buildLifeEvidenceSliceFromInputs(inputs);
|
|
33
33
|
const hardGuards = buildHardGuardDeps(continuity, inputs);
|
|
34
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
continuity,
|
|
36
|
+
lifeEvidence,
|
|
37
|
+
rhythmWindow,
|
|
38
|
+
hardGuards,
|
|
39
|
+
narrativeState: inputs.narrativeState,
|
|
40
|
+
relationshipMemory: inputs.relationshipMemory,
|
|
41
|
+
affordanceMap: inputs.affordanceMap,
|
|
42
|
+
identity: inputs.identity,
|
|
43
|
+
};
|
|
35
44
|
}
|
|
@@ -12,6 +12,7 @@ import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js"
|
|
|
12
12
|
import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
|
|
13
13
|
import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
|
|
14
14
|
import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
|
|
15
|
+
import type { AffordanceMap, IdentityProfile } from "../../../shared/types/v7-entities.js";
|
|
15
16
|
export interface SnapshotInputs {
|
|
16
17
|
mode: TopLevelMode;
|
|
17
18
|
currentWindowId: string;
|
|
@@ -58,6 +59,10 @@ export interface SnapshotInputs {
|
|
|
58
59
|
narrativeState?: NarrativeState;
|
|
59
60
|
/** When present, planner uses relationship memory to influence outreach timing. */
|
|
60
61
|
relationshipMemory?: RelationshipMemory;
|
|
62
|
+
/** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
|
|
63
|
+
affordanceMap?: AffordanceMap;
|
|
64
|
+
/** T-V7C.C.4: identity profile for connector request identity injection. */
|
|
65
|
+
identity?: IdentityProfile;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Build a ContinuitySnapshot from loaded inputs.
|
|
@@ -42,6 +42,27 @@ export function evaluateHardGuards(intent, runtime) {
|
|
|
42
42
|
if (!isSourceBacked(intent)) {
|
|
43
43
|
reasons.push("missing_source_refs");
|
|
44
44
|
}
|
|
45
|
+
// v7: Affordance / breaker guard (T-V7C.C.2)
|
|
46
|
+
if ((intent.effectClass === "connector_action" ||
|
|
47
|
+
intent.effectClass === "external_platform_action") &&
|
|
48
|
+
runtime.affordanceMap &&
|
|
49
|
+
intent.platformId) {
|
|
50
|
+
const platformItems = runtime.affordanceMap[intent.platformId] ?? [];
|
|
51
|
+
const match = intent.capabilityIntent
|
|
52
|
+
? platformItems.find((i) => i.capabilityId === intent.capabilityIntent)
|
|
53
|
+
: platformItems.find((i) => i.intent === intent.summary);
|
|
54
|
+
if (match) {
|
|
55
|
+
if (match.status === "painful") {
|
|
56
|
+
reasons.push("connector_circuit_open");
|
|
57
|
+
}
|
|
58
|
+
else if (match.status === "unavailable") {
|
|
59
|
+
reasons.push("affordance_unavailable");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
reasons.push("affordance_unavailable");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
const key = intentFingerprint(intent);
|
|
46
67
|
if (runtime.hardGuards.hasDuplicateIntent(key)) {
|
|
47
68
|
reasons.push("duplicate_intent");
|
|
@@ -74,7 +95,9 @@ export function evaluateHardGuards(intent, runtime) {
|
|
|
74
95
|
}
|
|
75
96
|
const duplicate = reasons.includes("duplicate_intent");
|
|
76
97
|
const cooldown = reasons.includes("outreach_cooldown");
|
|
77
|
-
|
|
98
|
+
const circuitOpen = reasons.includes("connector_circuit_open");
|
|
99
|
+
const affordanceUnavailable = reasons.includes("affordance_unavailable");
|
|
100
|
+
if (duplicate || cooldown || circuitOpen || affordanceUnavailable) {
|
|
78
101
|
return {
|
|
79
102
|
verdict: "defer",
|
|
80
103
|
reasons,
|
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quiet / reflection orchestration: empty evidence → empty_state; otherwise coverage-gated artifact (T2.3.3).
|
|
3
|
+
*
|
|
4
|
+
* v7 T-V7C.C.3: After a successful Quiet artifact write, if a DreamSchedulePort is provided,
|
|
5
|
+
* automatically trigger scheduleDream(quiet_completion). Skip reason is embedded in HeartbeatCycleResult
|
|
6
|
+
* reasons when the scheduler returns "skipped" (e.g. lock held).
|
|
3
7
|
*/
|
|
4
8
|
import type { CandidateIntent } from "../types.js";
|
|
5
9
|
import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
|
|
6
10
|
import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
|
|
7
11
|
import { type QuietArtifactAck } from "../../../storage/quiet/quiet-artifact-writer.js";
|
|
8
12
|
import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Minimal port for triggering Dream after Quiet completion (T-V7C.C.3).
|
|
15
|
+
* Kept narrow so run-source-backed-quiet does not take a hard dependency on dream-scheduler.
|
|
16
|
+
*/
|
|
17
|
+
export interface QuietDreamSchedulePort {
|
|
18
|
+
scheduleDream(params: {
|
|
19
|
+
triggerKind: "quiet_completion";
|
|
20
|
+
runId: string;
|
|
21
|
+
traceId: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
status: "started" | "skipped" | "queued";
|
|
24
|
+
reason?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
9
27
|
export interface RunSourceBackedQuietParams {
|
|
10
28
|
candidate: CandidateIntent;
|
|
11
29
|
runtime: HeartbeatRuntimeSnapshot;
|
|
12
30
|
day: string;
|
|
13
31
|
userInterestSnapshot?: UserInterestSnapshot;
|
|
14
32
|
workspaceRoot?: string;
|
|
33
|
+
/** v7 T-V7C.C.3: when present, a successful Quiet artifact write auto-triggers Dream scheduling. */
|
|
34
|
+
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
15
35
|
}
|
|
16
36
|
export interface RunSourceBackedQuietResult {
|
|
17
37
|
result: HeartbeatCycleResult;
|