@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.
Files changed (38) hide show
  1. package/agent-inner-guide.md +25 -0
  2. package/index.js +1 -1
  3. package/openclaw.plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/runtime/cli/commands/goal.d.ts +1 -0
  6. package/runtime/cli/commands/goal.js +1 -0
  7. package/runtime/cli/index.js +3 -3
  8. package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
  9. package/runtime/cli/ops/heartbeat-surface.js +2 -0
  10. package/runtime/cli/ops/ops-router.js +101 -1
  11. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +24 -0
  12. package/runtime/cli/ops/workspace-heartbeat-runner.js +42 -1
  13. package/runtime/connectors/base/contract.d.ts +10 -0
  14. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
  15. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +25 -0
  16. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +5 -0
  17. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +10 -1
  18. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +5 -0
  19. package/runtime/core/second-nature/orchestrator/guard-layer.js +24 -1
  20. package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +20 -0
  21. package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +32 -2
  22. package/runtime/guidance/capability-class.d.ts +38 -0
  23. package/runtime/guidance/capability-class.js +65 -0
  24. package/runtime/guidance/guidance-assembler.d.ts +2 -0
  25. package/runtime/guidance/guidance-assembler.js +16 -4
  26. package/runtime/guidance/guidance-draft-service.js +5 -5
  27. package/runtime/guidance/impulse-assembler.d.ts +71 -0
  28. package/runtime/guidance/impulse-assembler.js +103 -0
  29. package/runtime/guidance/index.d.ts +2 -0
  30. package/runtime/guidance/index.js +2 -0
  31. package/runtime/guidance/outreach-strategy-selector.d.ts +13 -0
  32. package/runtime/guidance/outreach-strategy-selector.js +2 -2
  33. package/runtime/guidance/template-registry.d.ts +15 -2
  34. package/runtime/guidance/template-registry.js +38 -1
  35. package/runtime/guidance/types.d.ts +13 -1
  36. package/runtime/storage/goal/agent-goal-store.d.ts +2 -0
  37. package/runtime/storage/goal/agent-goal-store.js +28 -1
  38. package/runtime/storage/services/tool-experience-store.js +11 -11
@@ -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.34";
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. */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "second-nature",
3
3
  "name": "Second Nature",
4
- "version": "0.1.34",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haaaiawd/second-nature",
3
- "version": "0.1.34",
3
+ "version": "0.1.38",
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",
@@ -8,6 +8,7 @@ export interface GoalCommandInput {
8
8
  criteria?: string;
9
9
  risk?: "low" | "medium" | "high";
10
10
  kind?: "short_term" | "long_term";
11
+ scope?: string;
11
12
  statusFilter?: string;
12
13
  originFilter?: string;
13
14
  limit?: number;
@@ -50,6 +50,7 @@ export async function goalCommand(stateDb, input) {
50
50
  await store.upsertAgentGoal({
51
51
  goalId,
52
52
  kind: input.kind ?? "short_term",
53
+ scope: input.scope?.trim() || "global",
53
54
  status: "accepted",
54
55
  origin: "owner_set",
55
56
  description,
@@ -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
- ? { workspaceRoot: options.workspaceRoot }
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 { continuity, lifeEvidence, rhythmWindow, hardGuards, narrativeState: inputs.narrativeState, relationshipMemory: inputs.relationshipMemory };
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
- if (duplicate || cooldown) {
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;