@haaaiawd/second-nature 0.1.25 → 0.1.27
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/SKILL.md +33 -0
- package/agent-inner-guide.md +124 -0
- package/index.js +206 -2
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -1
- package/runtime/cli/commands/goal.d.ts +2 -0
- package/runtime/cli/commands/goal.js +5 -1
- package/runtime/cli/commands/index.js +1 -1
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.js +13 -5
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
- package/runtime/cli/read-models/index.js +81 -10
- package/runtime/cli/read-models/types.d.ts +10 -3
- package/runtime/connectors/base/manifest.d.ts +77 -77
- package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
- package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +8 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +45 -4
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
- package/runtime/core/second-nature/index.d.ts +1 -0
- package/runtime/core/second-nature/index.js +1 -0
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +14 -2
- package/runtime/core/second-nature/orchestrator/goal-priority.js +2 -2
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
- package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
- package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
- package/runtime/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +96 -12
|
@@ -12,6 +12,8 @@ import { mapOperatorExplainToReadModel } from "./operator-explain-map.js";
|
|
|
12
12
|
import { loadOperatorFallbackRow, toOperatorFallbackView, } from "../../storage/fallback/load-operator-fallback.js";
|
|
13
13
|
import { loadRhythmPolicySnapshot, } from "../../storage/rhythm/rhythm-policy-snapshot.js";
|
|
14
14
|
import { createNarrativeStateStore } from "../../storage/narrative/narrative-state-store.js";
|
|
15
|
+
import { createRelationshipMemoryStore } from "../../storage/relationship/relationship-memory-store.js";
|
|
16
|
+
import { probeCredentialHealth } from "../../storage/services/credential-vault.js";
|
|
15
17
|
const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
|
|
16
18
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
17
19
|
function toExplainQuery(subject) {
|
|
@@ -31,6 +33,8 @@ function toExplainQuery(subject) {
|
|
|
31
33
|
return { kind: "delivery", auditId: subject.id };
|
|
32
34
|
case "source_ref":
|
|
33
35
|
return { kind: "source_ref", sourceRefId: subject.id };
|
|
36
|
+
case "relationship":
|
|
37
|
+
return { kind: "relationship", relationshipId: subject.id };
|
|
34
38
|
default:
|
|
35
39
|
return undefined;
|
|
36
40
|
}
|
|
@@ -47,6 +51,8 @@ function buildCredentialNextStep(status) {
|
|
|
47
51
|
return "submit_verification_answer";
|
|
48
52
|
if (status === "expired" || status === "revoked" || status === "failed")
|
|
49
53
|
return "refresh_credential_context";
|
|
54
|
+
if (status === "decrypt_failed")
|
|
55
|
+
return "verify_or_re_create_credential_then_re_import";
|
|
50
56
|
return undefined;
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
@@ -207,12 +213,28 @@ async function buildBaseStatus(deps) {
|
|
|
207
213
|
: undefined,
|
|
208
214
|
},
|
|
209
215
|
connectors: connectorSummary,
|
|
210
|
-
credentials: credentials.map((item) =>
|
|
211
|
-
platformId
|
|
212
|
-
item.platform_id
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
+
credentials: credentials.map((item) => {
|
|
217
|
+
const platformId = item.platformId ??
|
|
218
|
+
item.platform_id;
|
|
219
|
+
const encryptedValue = item.encryptedValue ??
|
|
220
|
+
item.encrypted_value;
|
|
221
|
+
const baseUrl = item.baseUrl ??
|
|
222
|
+
item.base_url;
|
|
223
|
+
const health = probeCredentialHealth(platformId, encryptedValue, baseUrl);
|
|
224
|
+
const effectiveStatus = health.state === "decrypt_failed"
|
|
225
|
+
? "decrypt_failed"
|
|
226
|
+
: item.status;
|
|
227
|
+
return {
|
|
228
|
+
platformId,
|
|
229
|
+
status: effectiveStatus,
|
|
230
|
+
nextStep: health.diagnosticCode === "missing_runtime_secret"
|
|
231
|
+
? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
|
|
232
|
+
: health.diagnosticCode === "credential_recovery_required"
|
|
233
|
+
? "verify_or_re_create_credential_then_re_import"
|
|
234
|
+
: buildCredentialNextStep(effectiveStatus),
|
|
235
|
+
keyHealth: health.keyHealth,
|
|
236
|
+
};
|
|
237
|
+
}),
|
|
216
238
|
risk: {
|
|
217
239
|
level: riskFlags.length > 0 ? "medium" : "low",
|
|
218
240
|
flags: riskFlags,
|
|
@@ -325,19 +347,40 @@ export function createCliReadModels(deps) {
|
|
|
325
347
|
record = undefined;
|
|
326
348
|
}
|
|
327
349
|
if (!record) {
|
|
350
|
+
// T1.4.1: even when no row exists, probe key health so status can surface
|
|
351
|
+
// missing_runtime_secret rather than a generic "missing".
|
|
352
|
+
const health = probeCredentialHealth(platformId, null, null);
|
|
328
353
|
return {
|
|
329
354
|
platformId,
|
|
330
|
-
status:
|
|
331
|
-
nextStep: "
|
|
355
|
+
status: health.state,
|
|
356
|
+
nextStep: health.diagnosticCode === "missing_runtime_secret"
|
|
357
|
+
? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
|
|
358
|
+
: "provide_credential_context",
|
|
359
|
+
keyHealth: health.keyHealth,
|
|
332
360
|
};
|
|
333
361
|
}
|
|
362
|
+
// T1.4.1: attempt decryption to detect decrypt_failed / wrong_key.
|
|
363
|
+
const encryptedValue = record.encryptedValue ??
|
|
364
|
+
record.encrypted_value;
|
|
365
|
+
const baseUrl = record.baseUrl ??
|
|
366
|
+
record.base_url;
|
|
367
|
+
const health = probeCredentialHealth(platformId, encryptedValue, baseUrl);
|
|
368
|
+
// If decryption failed, surface the honest diagnostic; otherwise surface DB status.
|
|
369
|
+
const effectiveStatus = health.state === "decrypt_failed"
|
|
370
|
+
? "decrypt_failed"
|
|
371
|
+
: record.status;
|
|
334
372
|
return {
|
|
335
373
|
platformId: record.platformId ??
|
|
336
374
|
record.platform_id,
|
|
337
|
-
status:
|
|
375
|
+
status: effectiveStatus,
|
|
338
376
|
verificationDeadline: record.expiresAt ?? undefined,
|
|
339
377
|
attemptsRemaining: record.attemptsRemaining ?? undefined,
|
|
340
|
-
nextStep:
|
|
378
|
+
nextStep: health.diagnosticCode === "missing_runtime_secret"
|
|
379
|
+
? "set_SECOND_NATURE_ENCRYPTION_KEY_then_re_probe"
|
|
380
|
+
: health.diagnosticCode === "credential_recovery_required"
|
|
381
|
+
? "verify_or_re_create_credential_then_re_import"
|
|
382
|
+
: buildCredentialNextStep(effectiveStatus),
|
|
383
|
+
keyHealth: health.keyHealth,
|
|
341
384
|
};
|
|
342
385
|
},
|
|
343
386
|
async loadFallbackView(ref) {
|
|
@@ -390,6 +433,34 @@ export function createCliReadModels(deps) {
|
|
|
390
433
|
evidenceRefs: [],
|
|
391
434
|
};
|
|
392
435
|
}
|
|
436
|
+
// T1.4.2: relationship explain reads RelationshipMemory store directly.
|
|
437
|
+
if (subject.kind === "relationship") {
|
|
438
|
+
const relationshipStore = createRelationshipMemoryStore(deps.stateDb);
|
|
439
|
+
const memory = await relationshipStore.loadRelationshipMemory(subject.id);
|
|
440
|
+
if (!memory) {
|
|
441
|
+
return {
|
|
442
|
+
subjectType: "relationship",
|
|
443
|
+
conclusion: "nothing_yet",
|
|
444
|
+
keyFactors: ["no_relationship_memory_recorded"],
|
|
445
|
+
evidenceRefs: [],
|
|
446
|
+
nextStep: "interact_with_agent_then_re_check",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
subjectType: "relationship",
|
|
451
|
+
conclusion: `tone:${memory.tonePreference} replies:${memory.noReplyCount === 0 ? "responsive" : "cooldown"}`,
|
|
452
|
+
keyFactors: [
|
|
453
|
+
`tone_preference:${memory.tonePreference}`,
|
|
454
|
+
...(memory.averageReplyDelayMinutes
|
|
455
|
+
? [`avg_reply_delay_minutes:${memory.averageReplyDelayMinutes}`]
|
|
456
|
+
: []),
|
|
457
|
+
...(memory.topicAffinities.length > 0
|
|
458
|
+
? [`topics:${memory.topicAffinities.map((t) => t.topic).join(",")}`]
|
|
459
|
+
: ["insufficient_history"]),
|
|
460
|
+
],
|
|
461
|
+
evidenceRefs: memory.sourceRefs.map((s) => s.sourceId),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
393
464
|
const query = subject.kind === "decision" ||
|
|
394
465
|
subject.kind === "platform-selection" ||
|
|
395
466
|
subject.kind === "outreach"
|
|
@@ -25,8 +25,10 @@ export interface ConnectorSummary {
|
|
|
25
25
|
}
|
|
26
26
|
export interface CredentialSummary {
|
|
27
27
|
platformId: string;
|
|
28
|
-
status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
|
|
28
|
+
status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
|
|
29
29
|
nextStep?: string;
|
|
30
|
+
/** T1.4.1 — diagnostic key health without leaking raw secret. */
|
|
31
|
+
keyHealth?: "missing_key" | "wrong_key" | "ok";
|
|
30
32
|
}
|
|
31
33
|
export interface RiskSummary {
|
|
32
34
|
level: "low" | "medium" | "high";
|
|
@@ -129,12 +131,17 @@ export interface SessionDetailReadModel {
|
|
|
129
131
|
}
|
|
130
132
|
export interface CredentialReadModel {
|
|
131
133
|
platformId: string;
|
|
132
|
-
status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed";
|
|
134
|
+
status: "missing" | "pending_verification" | "active" | "expired" | "revoked" | "failed" | "decrypt_failed";
|
|
133
135
|
verificationDeadline?: string;
|
|
134
136
|
attemptsRemaining?: number;
|
|
135
137
|
nextStep?: string;
|
|
138
|
+
/**
|
|
139
|
+
* T1.4.1 — redacted diagnostic: when true, the raw encrypted value could not be
|
|
140
|
+
* decrypted because SECOND_NATURE_ENCRYPTION_KEY is missing or wrong.
|
|
141
|
+
*/
|
|
142
|
+
keyHealth?: "missing_key" | "wrong_key" | "ok";
|
|
136
143
|
}
|
|
137
|
-
export type ExplainSubjectKind = "decision" | "platform-selection" | "outreach" | "soul-change" | "fallback" | "probe" | "delivery" | "report" | "source_ref";
|
|
144
|
+
export type ExplainSubjectKind = "decision" | "platform-selection" | "outreach" | "soul-change" | "fallback" | "probe" | "delivery" | "report" | "source_ref" | "relationship";
|
|
138
145
|
export interface ExplainReadModel {
|
|
139
146
|
subjectType: ExplainSubjectKind;
|
|
140
147
|
conclusion: string;
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { type CapabilityIntent, type ChannelType } from "./contract.js";
|
|
3
|
-
declare const connectorManifestSchema: z.ZodObject<{
|
|
4
|
-
platformId: z.ZodString;
|
|
5
|
-
supportedCapabilities: z.ZodArray<z.ZodEnum<{
|
|
6
|
-
"feed.read": "feed.read";
|
|
7
|
-
"post.publish": "post.publish";
|
|
8
|
-
"comment.reply": "comment.reply";
|
|
9
|
-
"notification.list": "notification.list";
|
|
10
|
-
"message.send": "message.send";
|
|
11
|
-
"agent.register": "agent.register";
|
|
12
|
-
"agent.heartbeat": "agent.heartbeat";
|
|
13
|
-
"work.discover": "work.discover";
|
|
14
|
-
"task.claim": "task.claim";
|
|
15
|
-
}>>;
|
|
16
|
-
channelPriority: z.ZodArray<z.ZodEnum<{
|
|
17
|
-
api_rest: "api_rest";
|
|
18
|
-
api_rpc: "api_rpc";
|
|
19
|
-
a2a: "a2a";
|
|
20
|
-
mcp: "mcp";
|
|
21
|
-
cli: "cli";
|
|
22
|
-
skill: "skill";
|
|
23
|
-
browser: "browser";
|
|
24
|
-
}>>;
|
|
25
|
-
credentialTypes: z.ZodArray<z.ZodString>;
|
|
26
|
-
degradedChannels: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
27
|
-
api_rest: "api_rest";
|
|
28
|
-
api_rpc: "api_rpc";
|
|
29
|
-
a2a: "a2a";
|
|
30
|
-
mcp: "mcp";
|
|
31
|
-
cli: "cli";
|
|
32
|
-
skill: "skill";
|
|
33
|
-
browser: "browser";
|
|
34
|
-
}>>>;
|
|
35
|
-
sourceRefPolicy: z.ZodOptional<z.ZodObject<{
|
|
36
|
-
minSourceRefs: z.ZodDefault<z.ZodNumber>;
|
|
37
|
-
rejectInlineSensitivePayload: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
-
}, z.core.$strip>>;
|
|
39
|
-
}, z.core.$strip>;
|
|
40
|
-
export type ConnectorManifest = z.infer<typeof connectorManifestSchema>;
|
|
41
|
-
export interface ResolvedConnectorCapability {
|
|
42
|
-
platformId: string;
|
|
43
|
-
intent: CapabilityIntent;
|
|
44
|
-
source: "namespace" | "v5_explicit" | "unambiguous_default";
|
|
45
|
-
}
|
|
46
|
-
export declare class CapabilityContractRegistry {
|
|
47
|
-
private readonly byPlatform;
|
|
48
|
-
register(manifest: ConnectorManifest): void;
|
|
49
|
-
loadManifest(platformId: string): ConnectorManifest;
|
|
50
|
-
listRegisteredPlatformIds(): string[];
|
|
51
|
-
hasCapability(platformId: string, intent: CapabilityIntent): boolean;
|
|
52
|
-
listCapabilities(platformId: string): CapabilityIntent[];
|
|
53
|
-
listChannels(platformId: string): ChannelType[];
|
|
54
|
-
/**
|
|
55
|
-
* Resolve a capability string that may be namespaced (`platformId:capability`)
|
|
56
|
-
* or a bare v5 capability. Returns the platform + intent pair.
|
|
57
|
-
* If bare capability and no explicit platform is provided, only succeeds when
|
|
58
|
-
* exactly one registered platform supports it (unambiguous_default).
|
|
59
|
-
*/
|
|
60
|
-
resolveCapability(intentWithNamespace: string, explicitPlatformId?: string): ResolvedConnectorCapability;
|
|
61
|
-
findPlatformsForIntent(intent: CapabilityIntent): string[];
|
|
62
|
-
}
|
|
63
|
-
/** T3.1.1 contract name for manifest-first registry. */
|
|
64
|
-
export declare const ConnectorManifestRegistry: typeof CapabilityContractRegistry;
|
|
65
|
-
export type ConnectorManifestRegistry = CapabilityContractRegistry;
|
|
66
|
-
export declare function describeConnector(registry: CapabilityContractRegistry, platformId: string): ConnectorManifest;
|
|
67
|
-
export declare function checkConnector(registry: CapabilityContractRegistry, platformId: string): {
|
|
68
|
-
ok: boolean;
|
|
69
|
-
errors: string[];
|
|
70
|
-
};
|
|
71
|
-
export declare function discoverCapabilities(registry: CapabilityContractRegistry): Array<{
|
|
72
|
-
platformId: string;
|
|
73
|
-
capabilities: CapabilityIntent[];
|
|
74
|
-
degradedChannels?: ChannelType[];
|
|
75
|
-
}>;
|
|
76
|
-
export declare function parseConnectorManifest(input: unknown): ConnectorManifest;
|
|
77
|
-
export {};
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type CapabilityIntent, type ChannelType } from "./contract.js";
|
|
3
|
+
declare const connectorManifestSchema: z.ZodObject<{
|
|
4
|
+
platformId: z.ZodString;
|
|
5
|
+
supportedCapabilities: z.ZodArray<z.ZodEnum<{
|
|
6
|
+
"feed.read": "feed.read";
|
|
7
|
+
"post.publish": "post.publish";
|
|
8
|
+
"comment.reply": "comment.reply";
|
|
9
|
+
"notification.list": "notification.list";
|
|
10
|
+
"message.send": "message.send";
|
|
11
|
+
"agent.register": "agent.register";
|
|
12
|
+
"agent.heartbeat": "agent.heartbeat";
|
|
13
|
+
"work.discover": "work.discover";
|
|
14
|
+
"task.claim": "task.claim";
|
|
15
|
+
}>>;
|
|
16
|
+
channelPriority: z.ZodArray<z.ZodEnum<{
|
|
17
|
+
api_rest: "api_rest";
|
|
18
|
+
api_rpc: "api_rpc";
|
|
19
|
+
a2a: "a2a";
|
|
20
|
+
mcp: "mcp";
|
|
21
|
+
cli: "cli";
|
|
22
|
+
skill: "skill";
|
|
23
|
+
browser: "browser";
|
|
24
|
+
}>>;
|
|
25
|
+
credentialTypes: z.ZodArray<z.ZodString>;
|
|
26
|
+
degradedChannels: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
27
|
+
api_rest: "api_rest";
|
|
28
|
+
api_rpc: "api_rpc";
|
|
29
|
+
a2a: "a2a";
|
|
30
|
+
mcp: "mcp";
|
|
31
|
+
cli: "cli";
|
|
32
|
+
skill: "skill";
|
|
33
|
+
browser: "browser";
|
|
34
|
+
}>>>;
|
|
35
|
+
sourceRefPolicy: z.ZodOptional<z.ZodObject<{
|
|
36
|
+
minSourceRefs: z.ZodDefault<z.ZodNumber>;
|
|
37
|
+
rejectInlineSensitivePayload: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
}, z.core.$strip>;
|
|
40
|
+
export type ConnectorManifest = z.infer<typeof connectorManifestSchema>;
|
|
41
|
+
export interface ResolvedConnectorCapability {
|
|
42
|
+
platformId: string;
|
|
43
|
+
intent: CapabilityIntent;
|
|
44
|
+
source: "namespace" | "v5_explicit" | "unambiguous_default";
|
|
45
|
+
}
|
|
46
|
+
export declare class CapabilityContractRegistry {
|
|
47
|
+
private readonly byPlatform;
|
|
48
|
+
register(manifest: ConnectorManifest): void;
|
|
49
|
+
loadManifest(platformId: string): ConnectorManifest;
|
|
50
|
+
listRegisteredPlatformIds(): string[];
|
|
51
|
+
hasCapability(platformId: string, intent: CapabilityIntent): boolean;
|
|
52
|
+
listCapabilities(platformId: string): CapabilityIntent[];
|
|
53
|
+
listChannels(platformId: string): ChannelType[];
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a capability string that may be namespaced (`platformId:capability`)
|
|
56
|
+
* or a bare v5 capability. Returns the platform + intent pair.
|
|
57
|
+
* If bare capability and no explicit platform is provided, only succeeds when
|
|
58
|
+
* exactly one registered platform supports it (unambiguous_default).
|
|
59
|
+
*/
|
|
60
|
+
resolveCapability(intentWithNamespace: string, explicitPlatformId?: string): ResolvedConnectorCapability;
|
|
61
|
+
findPlatformsForIntent(intent: CapabilityIntent): string[];
|
|
62
|
+
}
|
|
63
|
+
/** T3.1.1 contract name for manifest-first registry. */
|
|
64
|
+
export declare const ConnectorManifestRegistry: typeof CapabilityContractRegistry;
|
|
65
|
+
export type ConnectorManifestRegistry = CapabilityContractRegistry;
|
|
66
|
+
export declare function describeConnector(registry: CapabilityContractRegistry, platformId: string): ConnectorManifest;
|
|
67
|
+
export declare function checkConnector(registry: CapabilityContractRegistry, platformId: string): {
|
|
68
|
+
ok: boolean;
|
|
69
|
+
errors: string[];
|
|
70
|
+
};
|
|
71
|
+
export declare function discoverCapabilities(registry: CapabilityContractRegistry): Array<{
|
|
72
|
+
platformId: string;
|
|
73
|
+
capabilities: CapabilityIntent[];
|
|
74
|
+
degradedChannels?: ChannelType[];
|
|
75
|
+
}>;
|
|
76
|
+
export declare function parseConnectorManifest(input: unknown): ConnectorManifest;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T4.2.1 — Owner reply ingestion and RelationshipMemory feedback loop.
|
|
3
|
+
*
|
|
4
|
+
* When an owner replies to an outreach, this function:
|
|
5
|
+
* 1. Appends a `SessionChronicle` entry with the reply context.
|
|
6
|
+
* 2. Loads the current `RelationshipMemory`.
|
|
7
|
+
* 3. Infers tone/timing/topic from the reply text and updates the memory.
|
|
8
|
+
* 4. Persists the updated `RelationshipMemory` with source refs pointing to the chronicle entry.
|
|
9
|
+
*
|
|
10
|
+
* Boundaries:
|
|
11
|
+
* - Does NOT generate outreach drafts; that is the guidance layer's job.
|
|
12
|
+
* - Does NOT execute connectors; this is a pure state update path.
|
|
13
|
+
* - Errors in relationship update must not break chronicle write (chronicle is source of truth).
|
|
14
|
+
*/
|
|
15
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
16
|
+
import { type OwnerReplySignal } from "../../../storage/chronicle/session-chronicle-store.js";
|
|
17
|
+
import { type RelationshipMemory, type TopicAffinity } from "../../../storage/relationship/relationship-memory-store.js";
|
|
18
|
+
export interface ReplyInferenceConfig {
|
|
19
|
+
positiveKeywords?: string[];
|
|
20
|
+
negativeKeywords?: string[];
|
|
21
|
+
busyKeywords?: string[];
|
|
22
|
+
topicPatterns?: Record<string, string[]>;
|
|
23
|
+
}
|
|
24
|
+
export declare function inferTone(text: string, config?: ReplyInferenceConfig): "casual" | "direct" | "quiet" | "unknown";
|
|
25
|
+
export declare function inferTiming(text: string, config?: ReplyInferenceConfig): "responsive" | "busy" | undefined;
|
|
26
|
+
export declare function inferTopics(text: string, config?: ReplyInferenceConfig): string[];
|
|
27
|
+
export declare function mergeTopicAffinities(existing: TopicAffinity[], newTopics: string[]): TopicAffinity[];
|
|
28
|
+
export interface ProcessOwnerReplyInput {
|
|
29
|
+
/** The raw reply text from the owner. */
|
|
30
|
+
replyText: string;
|
|
31
|
+
/** The decisionId of the outreach this reply is responding to. */
|
|
32
|
+
relatedDecisionId: string;
|
|
33
|
+
/** Optional explicit owner signal (parsed by host or explicit UI). */
|
|
34
|
+
explicitSignal?: OwnerReplySignal;
|
|
35
|
+
}
|
|
36
|
+
export interface ProcessOwnerReplyResult {
|
|
37
|
+
chronicleEntryId: string;
|
|
38
|
+
relationshipUpdated: boolean;
|
|
39
|
+
priorMemory?: RelationshipMemory;
|
|
40
|
+
updatedMemory?: RelationshipMemory;
|
|
41
|
+
relationshipUpdateError?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Process an owner reply: write chronicle, update RelationshipMemory.
|
|
45
|
+
*/
|
|
46
|
+
export declare function processOwnerReply(input: ProcessOwnerReplyInput, state: StateDatabase): Promise<ProcessOwnerReplyResult>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createSessionChronicleStore, } from "../../../storage/chronicle/session-chronicle-store.js";
|
|
2
|
+
import { createRelationshipMemoryStore, } from "../../../storage/relationship/relationship-memory-store.js";
|
|
3
|
+
const DEFAULT_POSITIVE_KEYWORDS = [
|
|
4
|
+
"agree", "thanks", "appreciate", "helpful", "good", "great", "love", "like", "enjoy",
|
|
5
|
+
"excited", "happy", "nice", "wonderful", "awesome", "perfect", "cool", "ok", "yes",
|
|
6
|
+
];
|
|
7
|
+
const DEFAULT_NEGATIVE_KEYWORDS = [
|
|
8
|
+
"disagree", "frustrated", "annoying", "bad", "hate", "dislike", "angry", "upset",
|
|
9
|
+
"disappointed", "concerned", "no", "not", "never", "wrong", "terrible", "awful",
|
|
10
|
+
"useless", "stop", "don't",
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_BUSY_KEYWORDS = [
|
|
13
|
+
"busy", "swamped", "occupied", "tight schedule", "no time", "later",
|
|
14
|
+
"overloaded", "overwhelmed", "backlog", "not now", "another time", "schedule tight",
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_TOPIC_PATTERNS = {
|
|
17
|
+
work: ["work", "project", "task", "job", "delivery", "deadline"],
|
|
18
|
+
personal: ["family", "life", "health", "weekend", "trip"],
|
|
19
|
+
tech: ["code", "system", "bug", "feature", "architecture", "design"],
|
|
20
|
+
social: ["friend", "community", "meetup", "event", "collaboration"],
|
|
21
|
+
};
|
|
22
|
+
export function inferTone(text, config) {
|
|
23
|
+
const lower = text.toLowerCase();
|
|
24
|
+
const positiveKeywords = config?.positiveKeywords ?? DEFAULT_POSITIVE_KEYWORDS;
|
|
25
|
+
const negativeKeywords = config?.negativeKeywords ?? DEFAULT_NEGATIVE_KEYWORDS;
|
|
26
|
+
const pos = positiveKeywords.filter((w) => lower.includes(w)).length;
|
|
27
|
+
const neg = negativeKeywords.filter((w) => lower.includes(w)).length;
|
|
28
|
+
if (neg >= pos && neg > 0)
|
|
29
|
+
return "quiet"; // owner is negative → agent should be more reserved
|
|
30
|
+
if (pos > 0)
|
|
31
|
+
return "casual"; // positive → casual is fine
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
export function inferTiming(text, config) {
|
|
35
|
+
const lower = text.toLowerCase();
|
|
36
|
+
const busyKeywords = config?.busyKeywords ?? DEFAULT_BUSY_KEYWORDS;
|
|
37
|
+
if (busyKeywords.some((w) => lower.includes(w)))
|
|
38
|
+
return "busy";
|
|
39
|
+
if (lower.includes("quick") || lower.includes("prompt"))
|
|
40
|
+
return "responsive";
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
export function inferTopics(text, config) {
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
const topicPatterns = config?.topicPatterns ?? DEFAULT_TOPIC_PATTERNS;
|
|
46
|
+
const topics = [];
|
|
47
|
+
for (const [topic, patterns] of Object.entries(topicPatterns)) {
|
|
48
|
+
if (patterns.some((p) => lower.includes(p))) {
|
|
49
|
+
topics.push(topic);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return topics;
|
|
53
|
+
}
|
|
54
|
+
export function mergeTopicAffinities(existing, newTopics) {
|
|
55
|
+
const map = new Map(existing.map((t) => [t.topic, t.affinity]));
|
|
56
|
+
for (const topic of newTopics) {
|
|
57
|
+
map.set(topic, Math.min(1, (map.get(topic) ?? 0) + 0.1));
|
|
58
|
+
}
|
|
59
|
+
return Array.from(map.entries())
|
|
60
|
+
.map(([topic, affinity]) => ({ topic, affinity }))
|
|
61
|
+
.sort((a, b) => b.affinity - a.affinity);
|
|
62
|
+
}
|
|
63
|
+
function redactSensitive(text) {
|
|
64
|
+
return text
|
|
65
|
+
.replace(/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, "[REDACTED_CARD]")
|
|
66
|
+
.replace(/password[:\s=]+\S+/gi, "[REDACTED_PASSWORD]")
|
|
67
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[REDACTED_EMAIL]")
|
|
68
|
+
.replace(/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "[REDACTED_PHONE]")
|
|
69
|
+
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]")
|
|
70
|
+
.replace(/\b(?:sk-|pk-|Bearer\s+|api[_-]?key[:\s=]+)[A-Za-z0-9_\-\/+=]{20,}\b/gi, "[REDACTED_TOKEN]");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Process an owner reply: write chronicle, update RelationshipMemory.
|
|
74
|
+
*/
|
|
75
|
+
export async function processOwnerReply(input, state) {
|
|
76
|
+
const chronicleStore = createSessionChronicleStore(state);
|
|
77
|
+
const relStore = createRelationshipMemoryStore(state);
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
// 1. Write chronicle entry (source of truth)
|
|
80
|
+
const entryId = `owner_reply:${input.relatedDecisionId}:${Date.now()}`;
|
|
81
|
+
const replyText = input.replyText?.trim() ?? "";
|
|
82
|
+
const isEmpty = replyText.length === 0;
|
|
83
|
+
const tone = isEmpty ? "unknown" : inferTone(replyText);
|
|
84
|
+
const timing = isEmpty ? undefined : inferTiming(replyText);
|
|
85
|
+
const topics = isEmpty ? [] : inferTopics(replyText);
|
|
86
|
+
const chronicleEntry = {
|
|
87
|
+
entryId,
|
|
88
|
+
eventKind: "owner_reply",
|
|
89
|
+
actor: "owner",
|
|
90
|
+
occurredAt: now,
|
|
91
|
+
summary: redactSensitive(isEmpty ? "(empty reply)" : replyText.slice(0, 500)),
|
|
92
|
+
result: "succeeded",
|
|
93
|
+
sourceRefs: [{ sourceId: entryId, kind: "owner_reply", url: `chronicle://${entryId}` }],
|
|
94
|
+
relatedDecisionId: input.relatedDecisionId,
|
|
95
|
+
ownerReply: {
|
|
96
|
+
tone,
|
|
97
|
+
delayMinutes: input.explicitSignal?.delayMinutes,
|
|
98
|
+
topics: topics.length > 0 ? topics : input.explicitSignal?.topics,
|
|
99
|
+
explicitPreference: input.explicitSignal?.explicitPreference,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
await chronicleStore.appendSessionChronicle(chronicleEntry);
|
|
103
|
+
// 2. Load and update RelationshipMemory (best-effort)
|
|
104
|
+
let relationshipUpdated = false;
|
|
105
|
+
let priorMemory;
|
|
106
|
+
let updatedMemory;
|
|
107
|
+
try {
|
|
108
|
+
priorMemory = (await relStore.loadRelationshipMemory()) ?? undefined;
|
|
109
|
+
const nextRevision = (priorMemory?.revision ?? 0) + 1;
|
|
110
|
+
const topicAffinities = mergeTopicAffinities(priorMemory?.topicAffinities ?? [], topics);
|
|
111
|
+
const update = {
|
|
112
|
+
relationshipId: priorMemory?.relationshipId ?? "default",
|
|
113
|
+
revision: nextRevision,
|
|
114
|
+
tonePreference: tone !== "unknown" ? tone : (priorMemory?.tonePreference ?? "unknown"),
|
|
115
|
+
averageReplyDelayMinutes: input.explicitSignal?.delayMinutes ?? priorMemory?.averageReplyDelayMinutes,
|
|
116
|
+
noReplyCount: 0, // owner replied → reset counter
|
|
117
|
+
topicAffinities: topicAffinities.length > 0 ? topicAffinities : (priorMemory?.topicAffinities ?? []),
|
|
118
|
+
lastInteractionAt: now,
|
|
119
|
+
sourceRefs: [
|
|
120
|
+
...(priorMemory?.sourceRefs ?? []),
|
|
121
|
+
{ sourceId: entryId, kind: "owner_reply_feedback", url: `chronicle://${entryId}` },
|
|
122
|
+
],
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
};
|
|
125
|
+
await relStore.upsertRelationshipMemory(update);
|
|
126
|
+
updatedMemory = (await relStore.loadRelationshipMemory()) ?? undefined;
|
|
127
|
+
relationshipUpdated = true;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Relationship update is best-effort; chronicle is the source of truth.
|
|
131
|
+
// Missing memory update will be reflected in the next `explain relationship` query.
|
|
132
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
133
|
+
console.warn(`[owner-reply-feedback] RelationshipMemory update failed: ${errorMessage}`);
|
|
134
|
+
// Write a diagnostic chronicle entry so operators can trace the failure.
|
|
135
|
+
await chronicleStore.appendSessionChronicle({
|
|
136
|
+
entryId: `${entryId}:diagnostic`,
|
|
137
|
+
eventKind: "system_notice",
|
|
138
|
+
actor: "system",
|
|
139
|
+
occurredAt: new Date().toISOString(),
|
|
140
|
+
summary: `RelationshipMemory update failed: ${errorMessage}`,
|
|
141
|
+
result: "failed",
|
|
142
|
+
sourceRefs: [{ sourceId: entryId, kind: "owner_reply_feedback", url: `chronicle://${entryId}` }],
|
|
143
|
+
relatedDecisionId: input.relatedDecisionId,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
chronicleEntryId: entryId,
|
|
147
|
+
relationshipUpdated: false,
|
|
148
|
+
priorMemory,
|
|
149
|
+
updatedMemory,
|
|
150
|
+
relationshipUpdateError: errorMessage,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
chronicleEntryId: entryId,
|
|
155
|
+
relationshipUpdated,
|
|
156
|
+
priorMemory,
|
|
157
|
+
updatedMemory,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -20,6 +20,7 @@ import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.
|
|
|
20
20
|
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
21
21
|
import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
|
|
22
22
|
import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
23
|
+
import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
23
24
|
import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
24
25
|
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
25
26
|
export interface HeartbeatDecisionTracePayload {
|
|
@@ -47,7 +48,7 @@ export interface HeartbeatQuietWorkflowDeps {
|
|
|
47
48
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
48
49
|
* Exported for unit tests (CR-M1 wiring).
|
|
49
50
|
*/
|
|
50
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor">): Promise<HeartbeatCycleResult>;
|
|
51
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot">): Promise<HeartbeatCycleResult>;
|
|
51
52
|
export interface HeartbeatDeps {
|
|
52
53
|
/** Load snapshot inputs from state-system */
|
|
53
54
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -64,6 +65,12 @@ export interface HeartbeatDeps {
|
|
|
64
65
|
narrativeStateStore?: NarrativeStateStore;
|
|
65
66
|
/** T5.1.2: when present, heartbeat records a NarrativeTrace after successful narrative state update. */
|
|
66
67
|
recordNarrativeTrace?: (payload: NarrativeTracePayload) => Promise<void>;
|
|
68
|
+
/** T3.3.1: when present, successful connector effects write LifeEvidence artifacts. */
|
|
69
|
+
state?: StateDatabase;
|
|
70
|
+
/** T3.3.1: workspace root for evidence artifact paths. */
|
|
71
|
+
workspaceRoot?: string;
|
|
72
|
+
/** T2.4.1: when present, planner resolves platform-specific intents. */
|
|
73
|
+
connectorRegistry?: CapabilityContractRegistry;
|
|
67
74
|
}
|
|
68
75
|
/**
|
|
69
76
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -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
|
|
68
|
+
platformId: intent.platformId,
|
|
57
69
|
intent: toCapabilityIntent(intent),
|
|
58
70
|
payload: {},
|
|
59
|
-
decisionId
|
|
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
|
|
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;
|