@haaaiawd/second-nature 0.1.25 → 0.1.26

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 (34) hide show
  1. package/index.js +1 -0
  2. package/openclaw.plugin.json +1 -1
  3. package/package.json +1 -1
  4. package/runtime/cli/commands/goal.d.ts +2 -0
  5. package/runtime/cli/commands/goal.js +5 -1
  6. package/runtime/cli/commands/index.js +1 -1
  7. package/runtime/cli/explain/resolve-subject.js +3 -0
  8. package/runtime/cli/ops/ops-router.js +13 -5
  9. package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
  10. package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
  11. package/runtime/cli/read-models/index.js +81 -10
  12. package/runtime/cli/read-models/types.d.ts +10 -3
  13. package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
  14. package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
  15. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +8 -1
  16. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +45 -4
  17. package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
  18. package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
  19. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
  20. package/runtime/core/second-nature/index.d.ts +1 -0
  21. package/runtime/core/second-nature/index.js +1 -0
  22. package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +14 -2
  23. package/runtime/core/second-nature/orchestrator/goal-priority.js +2 -2
  24. package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
  25. package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
  26. package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
  27. package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
  28. package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
  29. package/runtime/observability/query/explain-query.d.ts +3 -0
  30. package/runtime/observability/query/explain-query.js +9 -0
  31. package/runtime/shared/types/credential.d.ts +1 -1
  32. package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
  33. package/runtime/storage/services/credential-vault.d.ts +18 -0
  34. package/runtime/storage/services/credential-vault.js +73 -3
@@ -8,6 +8,8 @@ import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-fro
8
8
  import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
9
9
  import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
10
10
  import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
11
+ import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
12
+ import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
11
13
  /**
12
14
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
13
15
  * Exported for unit tests (CR-M1 wiring).
@@ -52,19 +54,53 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
52
54
  intent.kind === "maintenance";
53
55
  const connectorUnwired = intent.effectClass === "connector_action";
54
56
  if (connectorUnwired && deps.connectorExecutor) {
57
+ if (!intent.platformId || intent.platformId === "unknown") {
58
+ return {
59
+ scope: "rhythm",
60
+ status: "intent_selected",
61
+ selectedIntentId: intent.id,
62
+ decisionId: `decision:${intent.id}:${Date.now()}`,
63
+ reasons: ["connector_dispatch_unavailable"],
64
+ };
65
+ }
66
+ const decisionId = `decision:${intent.id}:${Date.now()}`;
55
67
  const result = await deps.connectorExecutor.executeEffect({
56
- platformId: intent.platformId ?? "unknown",
68
+ platformId: intent.platformId,
57
69
  intent: toCapabilityIntent(intent),
58
70
  payload: {},
59
- decisionId: `decision:${intent.id}:${Date.now()}`,
71
+ decisionId,
60
72
  intentId: intent.id,
61
73
  idempotencyKey: `idem:${intent.id}:${Date.now()}`,
62
74
  });
75
+ // T3.3.1: on success, map connector result to life evidence and append.
76
+ // On failure or empty result, no evidence is fabricated — attempt audit
77
+ // is already recorded by the connector policy layer telemetry.
78
+ if (result.status === "success" &&
79
+ deps.state &&
80
+ deps.workspaceRoot) {
81
+ try {
82
+ const candidate = mapLifeEvidence({
83
+ platformId: intent.platformId,
84
+ intent: toCapabilityIntent(intent),
85
+ result,
86
+ observedAt: new Date().toISOString(),
87
+ });
88
+ if (candidate) {
89
+ await appendLifeEvidence(deps.state, deps.workspaceRoot, candidate);
90
+ }
91
+ }
92
+ catch (err) {
93
+ // Evidence append must not break the heartbeat cycle.
94
+ // Missing evidence will be reflected in the next snapshot load.
95
+ const errorMessage = err instanceof Error ? err.message : String(err);
96
+ console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
97
+ }
98
+ }
63
99
  const base = {
64
100
  scope: "rhythm",
65
101
  status: "intent_selected",
66
102
  selectedIntentId: intent.id,
67
- decisionId: `decision:${intent.id}:${Date.now()}`,
103
+ decisionId,
68
104
  reasons: result.status === "success"
69
105
  ? ["connector_effect_executed"]
70
106
  : result.status === "retryable_failure"
@@ -163,7 +199,12 @@ export async function ingestRhythmSignal(signal, deps) {
163
199
  const snapshot = buildContinuitySnapshot(inputs);
164
200
  const timestamp = signal.payload.timestamp;
165
201
  const runtime = buildHeartbeatRuntimeSnapshot(timestamp, inputs, snapshot);
166
- const rawCandidates = planCandidateIntents(runtime);
202
+ const rawCandidates = planCandidateIntents(runtime, {
203
+ acceptedGoals: inputs.acceptedGoals,
204
+ connectorRegistry: deps.connectorRegistry,
205
+ narrativeState: runtime.narrativeState,
206
+ relationshipMemory: runtime.relationshipMemory,
207
+ });
167
208
  const { candidates } = applyGoalPriority(rawCandidates, inputs.acceptedGoals);
168
209
  const emitTrace = async (result) => {
169
210
  if (!deps.recordDecisionTrace)
@@ -21,6 +21,8 @@ export interface HeartbeatRuntimeSnapshot {
21
21
  lifeEvidence: PlannerLifeEvidenceSlice;
22
22
  rhythmWindow: PlannerRhythmWindowSlice;
23
23
  hardGuards: HardGuardDeps;
24
+ narrativeState?: import("../../../storage/narrative/narrative-state-store.js").NarrativeState;
25
+ relationshipMemory?: import("../../../storage/relationship/relationship-memory-store.js").RelationshipMemory;
24
26
  }
25
27
  export declare function buildLifeEvidenceSliceFromInputs(inputs: SnapshotInputs): PlannerLifeEvidenceSlice;
26
28
  export declare function buildHardGuardDeps(continuity: ContinuitySnapshot, inputs: SnapshotInputs): HardGuardDeps;
@@ -31,5 +31,5 @@ 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 };
34
+ return { continuity, lifeEvidence, rhythmWindow, hardGuards, narrativeState: inputs.narrativeState, relationshipMemory: inputs.relationshipMemory };
35
35
  }
@@ -10,7 +10,8 @@ import type { ContinuitySnapshot, ControlPlaneSourceRef, TopLevelMode } from "..
10
10
  import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
11
11
  import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js";
12
12
  import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
13
- import type { AgentGoal } from "../../../storage/goal/agent-goal-store.js";
13
+ import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
14
+ import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
14
15
  export interface SnapshotInputs {
15
16
  mode: TopLevelMode;
16
17
  currentWindowId: string;
@@ -43,7 +44,20 @@ export interface SnapshotInputs {
43
44
  /** When present, outreach judgment uses this user-interest read model (T4.2.2). */
44
45
  userInterestSnapshot?: UserInterestSnapshot;
45
46
  /** T2.1.4: accepted goals to influence candidate intent priority. */
46
- acceptedGoals?: AgentGoal[];
47
+ acceptedGoals?: Array<{
48
+ goalId: string;
49
+ description: string;
50
+ completionCriteria?: string;
51
+ status: "proposal" | "accepted" | "rejected" | "completed" | "paused";
52
+ origin: "owner_set" | "agent_proposed" | "policy_seeded";
53
+ acceptedBy?: "owner" | "policy_allowlist";
54
+ }>;
55
+ /** When present, signals that acceptedGoals load failed (distinguishes from empty). */
56
+ acceptedGoalsLoadError?: string;
57
+ /** When present, planner uses narrative focus to influence candidate priority. */
58
+ narrativeState?: NarrativeState;
59
+ /** When present, planner uses relationship memory to influence outreach timing. */
60
+ relationshipMemory?: RelationshipMemory;
47
61
  }
48
62
  /**
49
63
  * Build a ContinuitySnapshot from loaded inputs.
@@ -20,3 +20,4 @@ export * from "./outreach/judge-input-from-snapshot.js";
20
20
  export * from "./quiet/run-source-backed-quiet.js";
21
21
  export * from "./guidance/request-guidance.js";
22
22
  export * from "./guidance/apply-guidance.js";
23
+ export * from "./feedback/owner-reply-feedback.js";
@@ -20,3 +20,4 @@ export * from "./outreach/judge-input-from-snapshot.js";
20
20
  export * from "./quiet/run-source-backed-quiet.js";
21
21
  export * from "./guidance/request-guidance.js";
22
22
  export * from "./guidance/apply-guidance.js";
23
+ export * from "./feedback/owner-reply-feedback.js";
@@ -8,7 +8,19 @@
8
8
  * All other statuses (proposal / rejected / completed / paused) are implicitly excluded.
9
9
  */
10
10
  import type { CandidateIntent } from "../types.js";
11
- import type { AgentGoal } from "../../../storage/goal/agent-goal-store.js";
11
+ /**
12
+ * Minimal goal context used by the priority module to avoid coupling
13
+ * to the full AgentGoal schema. M-03 decoupling.
14
+ */
15
+ export interface GoalPriorityContext {
16
+ goalId: string;
17
+ description: string;
18
+ completionCriteria?: string;
19
+ status: "proposal" | "accepted" | "rejected" | "completed" | "paused";
20
+ origin: "owner_set" | "agent_proposed" | "policy_seeded";
21
+ acceptedBy?: "owner" | "policy_allowlist";
22
+ }
23
+ export declare function isGoalRelatedToCandidate(goal: GoalPriorityContext, candidate: CandidateIntent): boolean;
12
24
  export interface ApplyGoalPriorityResult {
13
25
  candidates: CandidateIntent[];
14
26
  goalInfluences: Array<{
@@ -17,4 +29,4 @@ export interface ApplyGoalPriorityResult {
17
29
  boost: number;
18
30
  }>;
19
31
  }
20
- export declare function applyGoalPriority(candidates: CandidateIntent[], goals: AgentGoal[] | undefined): ApplyGoalPriorityResult;
32
+ export declare function applyGoalPriority(candidates: CandidateIntent[], goals: GoalPriorityContext[] | undefined): ApplyGoalPriorityResult;
@@ -6,8 +6,8 @@
6
6
  * range from 40–100, so 200 provides ample headroom without overflow.
7
7
  */
8
8
  const GOAL_PRIORITY_BOOST = 20;
9
- function isGoalRelatedToCandidate(goal, candidate) {
10
- const goalText = `${goal.description} ${goal.completionCriteria}`.toLowerCase();
9
+ export function isGoalRelatedToCandidate(goal, candidate) {
10
+ const goalText = `${goal.description} ${goal.completionCriteria ?? ""}`.toLowerCase();
11
11
  // Direct platformId mention in goal text
12
12
  if (candidate.platformId) {
13
13
  const platformId = candidate.platformId.toLowerCase();
@@ -4,10 +4,38 @@
4
4
  */
5
5
  import type { CandidateIntent, ContinuitySnapshot, DecisionBasis } from "../types.js";
6
6
  import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
7
+ import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
8
+ import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
9
+ import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
10
+ import { type PlatformResolutionContext } from "./platform-capability-router.js";
11
+ import { type GoalPriorityContext } from "./goal-priority.js";
12
+ /** Alias for GoalPriorityContext to keep intent-planner local naming consistent. M-03 decoupling. */
13
+ export type GoalContext = GoalPriorityContext;
14
+ export interface PlanIntentOptions {
15
+ narrativeState?: NarrativeState;
16
+ relationshipMemory?: RelationshipMemory;
17
+ budgetCheck?: boolean;
18
+ multiSource?: string[];
19
+ }
20
+ /**
21
+ * Factory for planning a candidate intent of a given kind.
22
+ * M-04: consolidates the previously separate plan{Work,Exploration,Social,Outreach}Intents.
23
+ */
24
+ export declare function planIntentWithKind(kind: "work" | "exploration" | "social" | "outreach", basePriority: number, runtime: HeartbeatRuntimeSnapshot, context: PlatformResolutionContext, registry?: CapabilityContractRegistry, options?: PlanIntentOptions): CandidateIntent[];
25
+ export interface PlanCandidateIntentsOptions {
26
+ /** T2.4.1: accepted goals for platform-specific resolution. */
27
+ acceptedGoals?: GoalContext[];
28
+ /** T2.4.1: optional connector registry for capability validation. */
29
+ connectorRegistry?: CapabilityContractRegistry;
30
+ /** CR-02: optional narrative state to influence candidate priority. */
31
+ narrativeState?: NarrativeState;
32
+ /** CR-02: optional relationship memory to influence outreach timing. */
33
+ relationshipMemory?: RelationshipMemory;
34
+ }
7
35
  /**
8
36
  * Plan ordered candidates for one heartbeat turn using rhythm window + life evidence slice.
9
37
  */
10
- export declare function planCandidateIntents(runtime: HeartbeatRuntimeSnapshot): CandidateIntent[];
38
+ export declare function planCandidateIntents(runtime: HeartbeatRuntimeSnapshot, options?: PlanCandidateIntentsOptions): CandidateIntent[];
11
39
  /** @deprecated Continuity-only helper for tests; prefer `planCandidateIntents` + `buildHeartbeatRuntimeSnapshot`. */
12
40
  export declare function planIntent(snapshot: ContinuitySnapshot): CandidateIntent[];
13
41
  export declare function decideDecisionBasis(intent: CandidateIntent): DecisionBasis;
@@ -1,5 +1,7 @@
1
1
  import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
2
2
  import { buildHeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
3
+ import { resolvePlatformForIntent, } from "./platform-capability-router.js";
4
+ import { isGoalRelatedToCandidate } from "./goal-priority.js";
3
5
  const MAX_CANDIDATE_INTENTS = 6;
4
6
  const OBLIGATION_SOURCE = [
5
7
  { id: "obligation-anchor", kind: "workspace_artifact", uri: "workspace://obligations/pending" },
@@ -8,72 +10,131 @@ function evidenceRefsForConnector(runtime) {
8
10
  if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence) && runtime.lifeEvidence.evidenceRefs.length > 0) {
9
11
  return runtime.lifeEvidence.evidenceRefs.slice(0, 8);
10
12
  }
11
- if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence)) {
12
- return [
13
- {
14
- id: "life-evidence-summary",
15
- kind: "connector_result",
16
- uri: `workspace://life-evidence/counts/${runtime.lifeEvidence.platformEventCount}/${runtime.lifeEvidence.workEventCount}`,
17
- },
18
- ];
19
- }
20
13
  return [];
21
14
  }
22
15
  function isAllowedKind(kind, runtime) {
23
16
  return runtime.rhythmWindow.allowedIntentKinds.includes(kind);
24
17
  }
25
- function planWorkIntents(runtime) {
26
- if (!isAllowedKind("work", runtime))
27
- return [];
28
- return runtime.continuity.pendingObligations.map((obligation, index) => ({
29
- id: `intent-obligation-${index}`,
30
- kind: "work",
31
- priority: 100 - index,
18
+ function focusMatchesKind(focus, kind) {
19
+ const lower = focus.toLowerCase();
20
+ switch (kind) {
21
+ case "work":
22
+ return lower.includes("work") || lower.includes("obligation") || lower.includes("task");
23
+ case "exploration":
24
+ return lower.includes("explor") || lower.includes("opportunit") || lower.includes("scan") || lower.includes("discover");
25
+ case "social":
26
+ return lower.includes("social") || lower.includes("engage") || lower.includes("community");
27
+ case "outreach":
28
+ return lower.includes("outreach") || lower.includes("user") || lower.includes("proactive") || lower.includes("contact");
29
+ case "quiet":
30
+ return lower.includes("quiet") || lower.includes("bookkeep") || lower.includes("pause");
31
+ case "reflection":
32
+ return lower.includes("reflect") || lower.includes("narrative") || lower.includes("review");
33
+ case "maintenance":
34
+ return lower.includes("maintenance") || lower.includes("check") || lower.includes("upkeep");
35
+ default:
36
+ return false;
37
+ }
38
+ }
39
+ const INTENT_CONFIGS = {
40
+ work: {
41
+ basePriority: 100,
42
+ effectClass: "connector_action",
43
+ summary: (platformId, detail) => platformId ? `fulfill obligation on ${platformId}: ${detail}` : `fulfill obligation: ${detail}`,
32
44
  source: "obligation",
33
- summary: `fulfill obligation: ${obligation}`,
45
+ idPrefix: "intent-obligation",
46
+ idempotencyPrefix: "obligation",
47
+ },
48
+ exploration: {
49
+ basePriority: 70,
34
50
  effectClass: "connector_action",
35
- sourceRefs: [...OBLIGATION_SOURCE],
36
- idempotencyKey: `obligation:${obligation}:${index}`,
37
- goalInfluenceRefs: [],
38
- }));
39
- }
40
- function planExplorationIntents(runtime) {
41
- if (!isAllowedKind("exploration", runtime))
51
+ summary: (platformId) => platformId ? `scan platform opportunities on ${platformId}` : "scan platform opportunities",
52
+ source: "tick",
53
+ idPrefix: "intent-exploration",
54
+ idempotencyPrefix: "exploration",
55
+ },
56
+ social: {
57
+ basePriority: 60,
58
+ effectClass: "connector_action",
59
+ summary: (platformId) => platformId ? `engage social platforms on ${platformId}` : "engage social platforms",
60
+ source: "tick",
61
+ idPrefix: "intent-social",
62
+ idempotencyPrefix: "social",
63
+ },
64
+ outreach: {
65
+ basePriority: 40,
66
+ effectClass: "user_outreach",
67
+ summary: (platformId) => platformId ? `consider proactive user outreach on ${platformId}` : "consider proactive user outreach",
68
+ source: "tick",
69
+ idPrefix: "intent-outreach",
70
+ idempotencyPrefix: "outreach",
71
+ },
72
+ };
73
+ /**
74
+ * Factory for planning a candidate intent of a given kind.
75
+ * M-04: consolidates the previously separate plan{Work,Exploration,Social,Outreach}Intents.
76
+ */
77
+ export function planIntentWithKind(kind, basePriority, runtime, context, registry, options) {
78
+ if (!isAllowedKind(kind, runtime))
42
79
  return [];
43
- const refs = evidenceRefsForConnector(runtime);
44
- return [
45
- {
46
- id: "intent-exploration",
47
- kind: "exploration",
48
- priority: 70,
49
- source: "tick",
50
- summary: "scan platform opportunities",
51
- effectClass: "connector_action",
52
- sourceRefs: refs,
53
- idempotencyKey: "exploration:scan platform opportunities",
80
+ const config = INTENT_CONFIGS[kind];
81
+ const platformId = resolvePlatformForIntent(kind, context ?? {}, registry);
82
+ let priority = basePriority;
83
+ // Social budget exhaustion → cap priority.
84
+ if (kind === "social" &&
85
+ runtime.continuity.budgets &&
86
+ runtime.continuity.budgets.socialUsed >= runtime.continuity.budgets.socialLimit) {
87
+ priority = 10;
88
+ }
89
+ // Narrative focus bias (preserved from original per-kind functions).
90
+ if (options?.narrativeState?.focus && focusMatchesKind(options.narrativeState.focus, kind)) {
91
+ priority += 15;
92
+ }
93
+ // Outreach suppression checks.
94
+ if (kind === "outreach") {
95
+ if (runtime.continuity.recentOutreachHashes.length > 3) {
96
+ return [];
97
+ }
98
+ if (options?.relationshipMemory && options.relationshipMemory.noReplyCount > 3) {
99
+ return [];
100
+ }
101
+ }
102
+ // Work special case: multi-source from pending obligations.
103
+ if (kind === "work" && options?.multiSource) {
104
+ return options.multiSource.map((source, index) => ({
105
+ id: platformId ? `${config.idPrefix}-${platformId}-${index}` : `${config.idPrefix}-${index}`,
106
+ kind: "work",
107
+ priority: basePriority - index,
108
+ source: "obligation",
109
+ platformId,
110
+ summary: config.summary(platformId, source),
111
+ effectClass: config.effectClass,
112
+ sourceRefs: [...OBLIGATION_SOURCE],
113
+ idempotencyKey: platformId
114
+ ? `${config.idempotencyPrefix}:${platformId}:${source}:${index}`
115
+ : `${config.idempotencyPrefix}:${source}:${index}`,
54
116
  goalInfluenceRefs: [],
55
- },
56
- ];
57
- }
58
- function planSocialIntents(runtime) {
59
- if (!isAllowedKind("social", runtime))
60
- return [];
61
- const refs = evidenceRefsForConnector(runtime);
117
+ }));
118
+ }
119
+ const refs = kind === "work" ? [...OBLIGATION_SOURCE] : evidenceRefsForConnector(runtime);
62
120
  return [
63
121
  {
64
- id: "intent-social",
65
- kind: "social",
66
- priority: runtime.continuity.budgets && runtime.continuity.budgets.socialUsed >= runtime.continuity.budgets.socialLimit ? 10 : 60,
67
- source: "tick",
68
- summary: "engage social platforms",
69
- effectClass: "connector_action",
122
+ id: platformId ? `${config.idPrefix}-${platformId}` : config.idPrefix,
123
+ kind,
124
+ priority,
125
+ source: config.source,
126
+ platformId,
127
+ summary: config.summary(platformId),
128
+ effectClass: config.effectClass,
70
129
  sourceRefs: refs,
71
- idempotencyKey: "social:engage social platforms",
130
+ idempotencyKey: platformId
131
+ ? `${config.idempotencyPrefix}:${platformId}`
132
+ : `${config.idempotencyPrefix}:${config.summary(undefined)}`,
72
133
  goalInfluenceRefs: [],
73
134
  },
74
135
  ];
75
136
  }
76
- function planQuietReflectionIntents(runtime) {
137
+ function planQuietReflectionIntents(runtime, _context, _registry) {
77
138
  if (!runtime.rhythmWindow.quietBias && runtime.continuity.mode !== "quiet") {
78
139
  return [];
79
140
  }
@@ -120,31 +181,17 @@ function planQuietReflectionIntents(runtime) {
120
181
  }
121
182
  return out;
122
183
  }
123
- function planOutreachIntents(runtime) {
124
- if (!isAllowedKind("outreach", runtime))
125
- return [];
126
- if (runtime.continuity.recentOutreachHashes.length > 3) {
127
- return [];
128
- }
129
- const refs = evidenceRefsForConnector(runtime);
130
- return [
131
- {
132
- id: "intent-outreach",
133
- kind: "outreach",
134
- priority: 40,
135
- source: "tick",
136
- summary: "consider proactive user outreach",
137
- effectClass: "user_outreach",
138
- sourceRefs: refs,
139
- idempotencyKey: "outreach:consider proactive user outreach",
140
- goalInfluenceRefs: [],
141
- },
142
- ];
143
- }
144
184
  /**
145
185
  * Plan ordered candidates for one heartbeat turn using rhythm window + life evidence slice.
146
186
  */
147
- export function planCandidateIntents(runtime) {
187
+ export function planCandidateIntents(runtime, options) {
188
+ const context = {
189
+ acceptedGoals: options?.acceptedGoals,
190
+ evidenceRefs: runtime.lifeEvidence.evidenceRefs,
191
+ };
192
+ const registry = options?.connectorRegistry;
193
+ const narrativeState = options?.narrativeState ?? runtime.narrativeState;
194
+ const relationshipMemory = options?.relationshipMemory ?? runtime.relationshipMemory;
148
195
  if (runtime.continuity.mode === "paused_for_interrupt") {
149
196
  const pausedMaintenance = [
150
197
  {
@@ -164,16 +211,44 @@ export function planCandidateIntents(runtime) {
164
211
  .slice(0, MAX_CANDIDATE_INTENTS);
165
212
  }
166
213
  if (runtime.continuity.mode === "maintenance_only") {
167
- return planWorkIntents(runtime).sort((a, b) => b.priority - a.priority).slice(0, MAX_CANDIDATE_INTENTS);
214
+ return planIntentWithKind("work", INTENT_CONFIGS.work.basePriority, runtime, context, registry, { multiSource: runtime.continuity.pendingObligations })
215
+ .sort((a, b) => b.priority - a.priority)
216
+ .slice(0, MAX_CANDIDATE_INTENTS);
168
217
  }
169
218
  const intents = [
170
- ...planWorkIntents(runtime),
171
- ...planExplorationIntents(runtime),
172
- ...planSocialIntents(runtime),
173
- ...planQuietReflectionIntents(runtime),
174
- ...planOutreachIntents(runtime),
219
+ ...planIntentWithKind("work", INTENT_CONFIGS.work.basePriority, runtime, context, registry, { multiSource: runtime.continuity.pendingObligations }),
220
+ ...planIntentWithKind("exploration", INTENT_CONFIGS.exploration.basePriority, runtime, context, registry),
221
+ ...planIntentWithKind("social", INTENT_CONFIGS.social.basePriority, runtime, context, registry, {
222
+ narrativeState,
223
+ budgetCheck: true,
224
+ }),
225
+ ...planQuietReflectionIntents(runtime, context, registry),
226
+ ...planIntentWithKind("outreach", INTENT_CONFIGS.outreach.basePriority, runtime, context, registry, {
227
+ narrativeState,
228
+ relationshipMemory,
229
+ }),
175
230
  ];
176
- return intents
231
+ // Pre-fill goalInfluenceRefs for non-obligation intents before returning.
232
+ // applyGoalPriority will later refine/override with the same logic.
233
+ const acceptedGoals = options?.acceptedGoals?.filter((g) => g.status === "accepted" &&
234
+ (g.origin !== "agent_proposed" || g.acceptedBy === "policy_allowlist")) ?? [];
235
+ for (const intent of intents) {
236
+ if (intent.source === "obligation")
237
+ continue;
238
+ const related = acceptedGoals.filter((g) => isGoalRelatedToCandidate(g, intent));
239
+ if (related.length > 0) {
240
+ intent.goalInfluenceRefs = related.map((g) => g.goalId);
241
+ }
242
+ }
243
+ // CR-02: apply narrative-focus bias globally across all candidate kinds.
244
+ const adjusted = intents.map((intent) => {
245
+ let priority = intent.priority;
246
+ if (narrativeState?.focus && focusMatchesKind(narrativeState.focus, intent.kind)) {
247
+ priority += 15;
248
+ }
249
+ return { ...intent, priority };
250
+ });
251
+ return adjusted
177
252
  .filter((intent) => runtime.rhythmWindow.allowedIntentKinds.includes(intent.kind))
178
253
  .sort((a, b) => b.priority - a.priority)
179
254
  .slice(0, MAX_CANDIDATE_INTENTS);
@@ -11,19 +11,32 @@ function mapControlPlaneRefToSourceRef(ref) {
11
11
  /**
12
12
  * Compute narrative confidence based on source evidence.
13
13
  *
14
- * Formula: min(intentSources / 3, 1) + (lifeEvidenceSources > 0 ? 0.1 : 0)
14
+ * Formula: smooth sigmoid-like growth:
15
+ * - 0 sources → 0
16
+ * - 1 source → 0.35 (not 0.43; single-source is weak but non-zero)
17
+ * - 2 sources → 0.60 (beginning to be trustworthy)
18
+ * - 3 sources → 0.80 (strong confidence, not 1.0)
19
+ * - 4+ sources → 0.90 (cap below 1.0 to avoid false certainty)
20
+ * - Boost: +0.05 if corroborating life evidence exists
21
+ * - Hard cap at 0.95 (never claim 100% certainty from evidence count alone)
15
22
  *
16
- * Rationale:
17
- * - Base: 1/3 per intent source (3 sources = 100% confidence)
18
- * - Boost: +0.1 if any life evidence exists (signals corroboration)
19
- * - Capped at 1.0 (100%)
23
+ * Rationale: linear 1/3 per source produces unnatural jumps.
24
+ * Logarithmic growth better models diminishing returns per extra source.
20
25
  */
21
26
  function computeConfidence(intentSources, lifeEvidenceSources) {
22
27
  if (intentSources === 0 && lifeEvidenceSources === 0)
23
28
  return 0;
24
- const base = Math.min(intentSources / 3, 1);
25
- const boost = lifeEvidenceSources > 0 ? 0.1 : 0;
26
- return Math.min(base + boost, 1);
29
+ const base = intentSources === 0
30
+ ? 0
31
+ : intentSources === 1
32
+ ? 0.35
33
+ : intentSources === 2
34
+ ? 0.60
35
+ : intentSources === 3
36
+ ? 0.80
37
+ : 0.90;
38
+ const boost = lifeEvidenceSources > 0 ? 0.05 : 0;
39
+ return Math.min(base + boost, 0.95);
27
40
  }
28
41
  /**
29
42
  * Build the next NarrativeState revision from a completed heartbeat cycle.
@@ -48,7 +61,8 @@ export function updateNarrativeAfterEffect(input) {
48
61
  const sourceRefs = selectedIntent.sourceRefs.map(mapControlPlaneRefToSourceRef);
49
62
  if (hasIntentSources || hasLifeEvidence) {
50
63
  // Source-backed revision
51
- const progressEntry = `${selectedIntent.effectClass}: ${selectedIntent.summary}`;
64
+ // L-03: Use effectClass + id as dedup key instead of full summary text.
65
+ const progressEntry = `${selectedIntent.effectClass}: ${selectedIntent.id}`;
52
66
  const progress = [...(prior?.progress ?? [])];
53
67
  if (!progress.includes(progressEntry)) {
54
68
  progress.push(progressEntry);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * T2.4.1 — Platform-specific intent resolution.
3
+ *
4
+ * When accepted goals, narrative, or connector evidence point to a specific
5
+ * platform, the planner emits a `CandidateIntent` with an explicit
6
+ * `platformId`. If the platform cannot be inferred, the caller falls
7
+ * back to the generic connector_action path (platformId undefined).
8
+ *
9
+ * Boundaries:
10
+ * - Does NOT execute connectors; only resolves platform + capability.
11
+ * - Does NOT validate credentials; that is the guard layer's job.
12
+ * - Optional registry: when absent, resolution is best-effort from goals/evidence.
13
+ */
14
+ import type { IntentKind } from "../types.js";
15
+ import type { ControlPlaneSourceRef } from "../types.js";
16
+ import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
17
+ /** Minimal goal shape accepted by the router to avoid coupling to AgentGoal. M-03 decoupling. */
18
+ interface GoalRouterContext {
19
+ goalId: string;
20
+ description: string;
21
+ completionCriteria?: string;
22
+ }
23
+ export interface PlatformResolutionContext {
24
+ /** Accepted goals that may name a platform or capability. */
25
+ acceptedGoals?: GoalRouterContext[];
26
+ /** Evidence refs that may embed platform identity. */
27
+ evidenceRefs?: ControlPlaneSourceRef[];
28
+ }
29
+ /**
30
+ * Resolve an explicit platformId for a candidate intent kind.
31
+ * Returns `undefined` when no unambiguous platform can be inferred.
32
+ */
33
+ export declare function resolvePlatformForIntent(kind: IntentKind, context: PlatformResolutionContext, registry?: CapabilityContractRegistry): string | undefined;
34
+ export {};