@haaaiawd/second-nature 0.1.17 → 0.1.19
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/index.js +19 -8
- package/runtime/cli/ops/heartbeat-surface.d.ts +10 -0
- package/runtime/cli/ops/heartbeat-surface.js +11 -3
- package/runtime/cli/ops/ops-router.d.ts +10 -0
- package/runtime/cli/ops/ops-router.js +21 -6
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +33 -3
- package/runtime/cli/ops/workspace-heartbeat-runner.js +77 -10
- package/runtime/cli/read-models/index.d.ts +6 -0
- package/runtime/cli/read-models/index.js +129 -20
- package/runtime/cli/read-models/types.d.ts +33 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +30 -7
- package/runtime/core/second-nature/runtime/service-entry.js +1 -1
- package/runtime/observability/db/index.js +77 -77
- package/runtime/observability/index.d.ts +1 -0
- package/runtime/observability/index.js +1 -0
- package/runtime/observability/services/runtime-decision-recorder.d.ts +29 -0
- package/runtime/observability/services/runtime-decision-recorder.js +94 -0
- package/runtime/storage/db/index.js +93 -93
- package/workspace-ops-bridge.js +1 -0
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "second-nature",
|
|
3
3
|
"name": "Second Nature",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.19",
|
|
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 (see README / T1.1.4 ops norm).",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
package/package.json
CHANGED
package/runtime/cli/index.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
|
-
import { createObservabilityDatabase } from "../observability/db/index.js";
|
|
2
|
-
import { createStateAPI, createStateDatabase } from "../storage/index.js";
|
|
1
|
+
import { createObservabilityDatabase, } from "../observability/db/index.js";
|
|
2
|
+
import { createStateAPI, createStateDatabase, } from "../storage/index.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createActionBridge } from "./action-bridge.js";
|
|
5
|
-
import { createCliCommands } from "./commands/index.js";
|
|
5
|
+
import { createCliCommands, } from "./commands/index.js";
|
|
6
6
|
import { createOpsRouter } from "./ops/ops-router.js";
|
|
7
|
-
import { createCliReadModels } from "./read-models/index.js";
|
|
7
|
+
import { createCliReadModels, } from "./read-models/index.js";
|
|
8
8
|
import { resolvePackagedRuntime } from "./runtime/runtime-artifact-boundary.js";
|
|
9
|
+
import { createRuntimeDecisionRecorder, } from "../observability/services/runtime-decision-recorder.js";
|
|
9
10
|
export function createCliRuntimeDeps(overrides = {}) {
|
|
10
11
|
const stateDb = overrides.stateDb ?? createStateDatabase();
|
|
11
12
|
const observabilityDb = overrides.observabilityDb ?? createObservabilityDatabase();
|
|
12
13
|
const stateApi = overrides.stateApi ?? createStateAPI(stateDb);
|
|
13
|
-
const readModels = overrides.readModels ??
|
|
14
|
+
const readModels = overrides.readModels ??
|
|
15
|
+
createCliReadModels({
|
|
16
|
+
stateDb,
|
|
17
|
+
observabilityDb,
|
|
18
|
+
workspaceRoot: process.cwd(),
|
|
19
|
+
});
|
|
14
20
|
const actionBridge = overrides.actionBridge ?? createActionBridge(stateApi);
|
|
21
|
+
const runtimeRecorder = overrides.runtimeRecorder ?? createRuntimeDecisionRecorder(observabilityDb);
|
|
15
22
|
return {
|
|
16
23
|
stateDb,
|
|
17
24
|
observabilityDb,
|
|
18
25
|
stateApi,
|
|
19
26
|
readModels,
|
|
20
27
|
actionBridge,
|
|
28
|
+
runtimeRecorder,
|
|
21
29
|
};
|
|
22
30
|
}
|
|
23
31
|
export function createCommandRouter(options = {}) {
|
|
@@ -26,6 +34,9 @@ export function createCommandRouter(options = {}) {
|
|
|
26
34
|
const opsRouter = createOpsRouter({
|
|
27
35
|
runtimeAvailable: resolvePackagedRuntime(pluginRoot).ok,
|
|
28
36
|
readModels: runtime.readModels,
|
|
37
|
+
runtimeRecorder: runtime.runtimeRecorder,
|
|
38
|
+
state: runtime.stateDb,
|
|
39
|
+
workspaceRoot: process.cwd(),
|
|
29
40
|
});
|
|
30
41
|
const commands = createCliCommands({
|
|
31
42
|
readModels: runtime.readModels,
|
|
@@ -43,12 +54,12 @@ export function closeCliRuntimeDeps(deps) {
|
|
|
43
54
|
deps.stateDb.close();
|
|
44
55
|
deps.observabilityDb.close();
|
|
45
56
|
}
|
|
46
|
-
export { createOpsRouter } from "./ops/ops-router.js";
|
|
57
|
+
export { createOpsRouter, } from "./ops/ops-router.js";
|
|
47
58
|
export { heartbeatCheck, } from "./ops/heartbeat-surface.js";
|
|
48
59
|
export * from "./host-capability/types.js";
|
|
49
|
-
export { classifyDeliveryCapability } from "./host-capability/classify-delivery.js";
|
|
60
|
+
export { classifyDeliveryCapability, } from "./host-capability/classify-delivery.js";
|
|
50
61
|
export { probeHostCapability } from "./host-capability/probe-host-capability.js";
|
|
51
62
|
export { recordHostCapability } from "./host-capability/record-host-capability.js";
|
|
52
63
|
export { runHostSmoke } from "./host-smoke/run-host-smoke.js";
|
|
53
64
|
export { explainSurfaceSubject } from "./explain/explain-surface-subject.js";
|
|
54
|
-
export { showOperatorFallback, OperatorFallbackNotFoundError } from "./ops/show-operator-fallback.js";
|
|
65
|
+
export { showOperatorFallback, OperatorFallbackNotFoundError, } from "./ops/show-operator-fallback.js";
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { SurfaceMode } from "../runtime/runtime-artifact-boundary.js";
|
|
8
8
|
import type { HeartbeatSignal } from "../../core/second-nature/heartbeat/signal.js";
|
|
9
9
|
import type { CliReadModels } from "../read-models/index.js";
|
|
10
|
+
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
11
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
10
12
|
export type HeartbeatSurfaceStatus = "heartbeat_ok" | "intent_selected" | "denied" | "deferred" | "runtime_carrier_only" | "delivery_unavailable";
|
|
11
13
|
export interface HeartbeatSurfaceResult {
|
|
12
14
|
ok: boolean;
|
|
@@ -28,6 +30,14 @@ export interface HeartbeatCheckInput {
|
|
|
28
30
|
fakeControlPlanePassthrough?: Record<string, unknown>;
|
|
29
31
|
/** When set, full-runtime heartbeat_check runs the control-plane decision loop (US-001). */
|
|
30
32
|
readModels?: CliReadModels;
|
|
33
|
+
/** When set, full-runtime cycles are persisted so `loadStatus` exits unknown (T1.2.3). */
|
|
34
|
+
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
35
|
+
/**
|
|
36
|
+
* T2.2.2: when set together with `workspaceRoot`, life evidence from the state DB is loaded
|
|
37
|
+
* and merged into SnapshotInputs so planner/guard paths see real source-ref truth.
|
|
38
|
+
*/
|
|
39
|
+
state?: StateDatabase;
|
|
40
|
+
workspaceRoot?: string;
|
|
31
41
|
timestamp?: string;
|
|
32
42
|
sessionContext?: string;
|
|
33
43
|
scopeHint?: HeartbeatSignal["scopeHint"];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createWorkspaceHeartbeatRunner } from "./workspace-heartbeat-runner.js";
|
|
2
2
|
function mapCycleToSurface(cycle, surfaceMode) {
|
|
3
|
-
const status = cycle.status === "runtime_carrier_only"
|
|
3
|
+
const status = cycle.status === "runtime_carrier_only"
|
|
4
|
+
? "runtime_carrier_only"
|
|
5
|
+
: cycle.status;
|
|
4
6
|
return {
|
|
5
7
|
ok: true,
|
|
6
8
|
status,
|
|
@@ -62,10 +64,16 @@ export async function heartbeatCheck(input) {
|
|
|
62
64
|
scopeHint: input.scopeHint,
|
|
63
65
|
payload: {
|
|
64
66
|
timestamp,
|
|
65
|
-
sessionContext: typeof input.sessionContext === "string"
|
|
67
|
+
sessionContext: typeof input.sessionContext === "string"
|
|
68
|
+
? input.sessionContext
|
|
69
|
+
: undefined,
|
|
66
70
|
},
|
|
67
71
|
};
|
|
68
|
-
const run = createWorkspaceHeartbeatRunner(input.readModels
|
|
72
|
+
const run = createWorkspaceHeartbeatRunner(input.readModels, {
|
|
73
|
+
runtimeRecorder: input.runtimeRecorder,
|
|
74
|
+
state: input.state,
|
|
75
|
+
workspaceRoot: input.workspaceRoot ?? process.cwd(),
|
|
76
|
+
});
|
|
69
77
|
const cycle = await run(signal);
|
|
70
78
|
return mapCycleToSurface(cycle, "workspace_full_runtime");
|
|
71
79
|
}
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { type HeartbeatCheckInput, type HeartbeatSurfaceResult } from "./heartbeat-surface.js";
|
|
5
5
|
import type { CliReadModels } from "../read-models/index.js";
|
|
6
|
+
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
7
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
6
8
|
export interface OpsRouterDeps {
|
|
7
9
|
/** When true, packaged runtime artifacts resolved and full graph is loadable */
|
|
8
10
|
runtimeAvailable: boolean;
|
|
9
11
|
/** Workspace read models: fallback view + heartbeat decision loop inputs (T1.2.2 / US-001). */
|
|
10
12
|
readModels?: CliReadModels;
|
|
13
|
+
/** Persists full-runtime heartbeat cycles so `loadStatus` exits the unknown baseline (T1.2.3). */
|
|
14
|
+
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
15
|
+
/**
|
|
16
|
+
* T2.2.2: state DB + workspace root for life evidence loading in full-runtime heartbeat cycles.
|
|
17
|
+
* When set, `loadSnapshotInputsForWorkspaceHeartbeat` can fill `lifeEvidenceRefs` from real DB truth.
|
|
18
|
+
*/
|
|
19
|
+
state?: StateDatabase;
|
|
20
|
+
workspaceRoot?: string;
|
|
11
21
|
}
|
|
12
22
|
export interface OpsRouter {
|
|
13
23
|
heartbeatCheck(input: HeartbeatCheckInput): Promise<HeartbeatSurfaceResult>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared ops command dispatch for CLI + tool surfaces (T1.1.3, T1.2.2).
|
|
3
3
|
*/
|
|
4
|
-
import { heartbeatCheck } from "./heartbeat-surface.js";
|
|
5
|
-
import { showOperatorFallback, OperatorFallbackNotFoundError } from "./show-operator-fallback.js";
|
|
4
|
+
import { heartbeatCheck, } from "./heartbeat-surface.js";
|
|
5
|
+
import { showOperatorFallback, OperatorFallbackNotFoundError, } from "./show-operator-fallback.js";
|
|
6
6
|
function coerceProbeOnlyFlag(input) {
|
|
7
7
|
const v = input?.probeOnly;
|
|
8
8
|
return v === true || v === "true" || v === 1 || v === "1";
|
|
@@ -13,19 +13,34 @@ export function createOpsRouter(deps) {
|
|
|
13
13
|
...input,
|
|
14
14
|
runtimeAvailable: input.runtimeAvailable ?? deps.runtimeAvailable,
|
|
15
15
|
readModels: input.readModels ?? deps.readModels,
|
|
16
|
+
runtimeRecorder: input.runtimeRecorder ?? deps.runtimeRecorder,
|
|
17
|
+
state: input.state ?? deps.state,
|
|
18
|
+
workspaceRoot: input.workspaceRoot ?? deps.workspaceRoot,
|
|
16
19
|
}),
|
|
17
20
|
dispatch(command, input) {
|
|
18
21
|
if (command === "heartbeat_check") {
|
|
19
|
-
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
22
|
+
const runtimeAvailable = typeof input?.runtimeAvailable === "boolean"
|
|
23
|
+
? input.runtimeAvailable
|
|
24
|
+
: deps.runtimeAvailable;
|
|
20
25
|
return heartbeatCheck({
|
|
21
26
|
probeOnly: coerceProbeOnlyFlag(input),
|
|
22
27
|
runtimeAvailable,
|
|
23
|
-
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
|
|
28
|
+
fakeControlPlanePassthrough: input?.fakeControlPlanePassthrough &&
|
|
29
|
+
typeof input.fakeControlPlanePassthrough === "object"
|
|
24
30
|
? input.fakeControlPlanePassthrough
|
|
25
31
|
: undefined,
|
|
26
|
-
readModels: input?.readModels ??
|
|
32
|
+
readModels: input?.readModels ??
|
|
33
|
+
deps.readModels,
|
|
34
|
+
runtimeRecorder: input
|
|
35
|
+
?.runtimeRecorder ?? deps.runtimeRecorder,
|
|
36
|
+
state: input?.state ??
|
|
37
|
+
deps.state,
|
|
38
|
+
workspaceRoot: input
|
|
39
|
+
?.workspaceRoot ?? deps.workspaceRoot,
|
|
27
40
|
timestamp: typeof input?.timestamp === "string" ? input.timestamp : undefined,
|
|
28
|
-
sessionContext: typeof input?.sessionContext === "string"
|
|
41
|
+
sessionContext: typeof input?.sessionContext === "string"
|
|
42
|
+
? input.sessionContext
|
|
43
|
+
: undefined,
|
|
29
44
|
scopeHint: input?.scopeHint,
|
|
30
45
|
});
|
|
31
46
|
}
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Wires CLI read models into control-plane `runHeartbeatCycle` for `heartbeat_check` (US-001 / CH-09-02).
|
|
2
|
+
* Wires CLI read models into control-plane `runHeartbeatCycle` for `heartbeat_check` (US-001 / CH-09-02 / T1.2.3).
|
|
3
3
|
*
|
|
4
4
|
* Snapshot inputs are derived from aggregated status; delivery defaults to none until host capability is modeled here.
|
|
5
|
+
*
|
|
6
|
+
* T1.2.3: when a `RuntimeDecisionRecorder` is provided, persist a `sn-runtime-*` ledger row +
|
|
7
|
+
* `second-nature-runtime` execution attempt after each cycle so `loadStatus` exits its `unknown`
|
|
8
|
+
* baseline once the runtime has actually executed at least one full-runtime turn.
|
|
9
|
+
*
|
|
10
|
+
* T2.2.2: when `state` + `workspaceRoot` are supplied, call `loadLifeEvidenceSnapshot` to fill
|
|
11
|
+
* `lifeEvidenceRefs`, `platformEventCount`, `workEventCount`, and `lifeEvidenceEmptyReason` on
|
|
12
|
+
* `SnapshotInputs` so planner/guard paths that require source refs see real DB truth.
|
|
13
|
+
* Falls back gracefully to `lifeEvidenceEmptyReason: "state_unavailable"` when state is absent.
|
|
5
14
|
*/
|
|
6
15
|
import type { HeartbeatSignal, HeartbeatCycleResult } from "../../core/second-nature/heartbeat/signal.js";
|
|
7
16
|
import type { SnapshotInputs } from "../../core/second-nature/heartbeat/snapshot-builder.js";
|
|
8
17
|
import type { CliReadModels } from "../read-models/index.js";
|
|
9
|
-
|
|
10
|
-
|
|
18
|
+
import type { RuntimeDecisionRecorder } from "../../observability/services/runtime-decision-recorder.js";
|
|
19
|
+
import type { StateDatabase } from "../../storage/db/index.js";
|
|
20
|
+
export interface WorkspaceHeartbeatRunnerOptions {
|
|
21
|
+
/** When supplied, the runner persists the cycle so `loadStatus` can read it (T1.2.3). */
|
|
22
|
+
runtimeRecorder?: RuntimeDecisionRecorder;
|
|
23
|
+
/**
|
|
24
|
+
* T2.2.2: when state + workspaceRoot are provided, life evidence is loaded from DB and merged
|
|
25
|
+
* into SnapshotInputs so planner/guard paths have real source-ref truth.
|
|
26
|
+
*/
|
|
27
|
+
state?: StateDatabase;
|
|
28
|
+
workspaceRoot?: string;
|
|
29
|
+
/**
|
|
30
|
+
* T1.2.4: when true (and workspaceRoot is set), inject a `quietWorkflow` dep into the heartbeat
|
|
31
|
+
* cycle so quiet/reflection intents can call `runSourceBackedQuiet` and write artifacts to disk.
|
|
32
|
+
* Defaults to true when workspaceRoot is provided, since this is the host-safe workspace path.
|
|
33
|
+
*/
|
|
34
|
+
enableQuietWorkflow?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare function loadSnapshotInputsForWorkspaceHeartbeat(readModels: CliReadModels, options?: {
|
|
37
|
+
state?: StateDatabase;
|
|
38
|
+
workspaceRoot?: string;
|
|
39
|
+
}): Promise<SnapshotInputs>;
|
|
40
|
+
export declare function createWorkspaceHeartbeatRunner(readModels: CliReadModels, options?: WorkspaceHeartbeatRunnerOptions): (signal: HeartbeatSignal) => Promise<HeartbeatCycleResult>;
|
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import { runHeartbeatCycle } from "../../core/second-nature/heartbeat/run-heartbeat-cycle.js";
|
|
2
|
-
|
|
2
|
+
import { loadLifeEvidenceSnapshot } from "../../storage/snapshots/life-evidence-snapshot.js";
|
|
3
|
+
export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels, options = {}) {
|
|
3
4
|
const status = await readModels.loadStatus();
|
|
4
5
|
const mode = status.rhythm.mode === "unknown" ? "active" : status.rhythm.mode;
|
|
5
|
-
|
|
6
|
+
// CH-15-03: quietEnabledBridge should reflect whether the quiet *execution path* is wired
|
|
7
|
+
// (workspaceRoot available), not whether the last observed rhythm decision was "quiet".
|
|
8
|
+
// status.quiet.mode is typically "unknown" until a Quiet artifact has been persisted, which
|
|
9
|
+
// means binding to it would permanently suppress the quiet window — the opposite of intent.
|
|
10
|
+
// We instead enable the bridge whenever workspaceRoot is provided (same condition as
|
|
11
|
+
// `createWorkspaceHeartbeatRunner` uses for injecting quietWorkflow).
|
|
12
|
+
const quietEnabledBridge = !!options.workspaceRoot;
|
|
13
|
+
// T2.2.2: Load life evidence from state DB when available so SnapshotInputs carries real refs.
|
|
14
|
+
let lifeEvidenceRefs;
|
|
15
|
+
let platformEventCount;
|
|
16
|
+
let workEventCount;
|
|
17
|
+
let lifeEvidenceEmptyReason;
|
|
18
|
+
if (options.state && options.workspaceRoot) {
|
|
19
|
+
try {
|
|
20
|
+
const snapshot = await loadLifeEvidenceSnapshot(options.state, options.workspaceRoot, { limit: 50 },
|
|
21
|
+
// Skip repair gate here — runner is called inside a live cycle; gate ran at startup.
|
|
22
|
+
{ runRepairGate: false });
|
|
23
|
+
lifeEvidenceRefs = snapshot.evidenceRefs.map((ref) => ({
|
|
24
|
+
id: ref.id,
|
|
25
|
+
kind: ref.kind,
|
|
26
|
+
uri: ref.uri,
|
|
27
|
+
}));
|
|
28
|
+
platformEventCount = snapshot.platformEvents.length;
|
|
29
|
+
workEventCount = snapshot.workEvents.length;
|
|
30
|
+
if (snapshot.empty) {
|
|
31
|
+
lifeEvidenceEmptyReason = "no_sources";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// If evidence load fails, signal state_unavailable rather than crashing the cycle.
|
|
36
|
+
lifeEvidenceRefs = [];
|
|
37
|
+
platformEventCount = 0;
|
|
38
|
+
workEventCount = 0;
|
|
39
|
+
lifeEvidenceEmptyReason = "state_unavailable";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// No state wired — record that life evidence wasn't loaded so guards can reason honestly.
|
|
44
|
+
lifeEvidenceEmptyReason = "state_unavailable";
|
|
45
|
+
}
|
|
6
46
|
return {
|
|
7
47
|
mode,
|
|
8
48
|
currentWindowId: status.rhythm.windowId ?? "workspace-default",
|
|
@@ -13,14 +53,41 @@ export async function loadSnapshotInputsForWorkspaceHeartbeat(readModels) {
|
|
|
13
53
|
awaitingUserInput: false,
|
|
14
54
|
quietEnabledBridge,
|
|
15
55
|
deliveryCapability: { target: "none" },
|
|
56
|
+
lifeEvidenceRefs,
|
|
57
|
+
platformEventCount,
|
|
58
|
+
workEventCount,
|
|
59
|
+
lifeEvidenceEmptyReason,
|
|
16
60
|
};
|
|
17
61
|
}
|
|
18
|
-
export function createWorkspaceHeartbeatRunner(readModels) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
62
|
+
export function createWorkspaceHeartbeatRunner(readModels, options = {}) {
|
|
63
|
+
// T1.2.4: inject quietWorkflow dep when workspaceRoot is set so quiet/reflection intents
|
|
64
|
+
// can trigger runSourceBackedQuiet and persist artifacts to disk.
|
|
65
|
+
const quietEnabled = options.workspaceRoot && options.enableQuietWorkflow !== false;
|
|
66
|
+
return async (signal) => {
|
|
67
|
+
const cycle = await runHeartbeatCycle({
|
|
68
|
+
signal,
|
|
69
|
+
runtimeAvailable: true,
|
|
70
|
+
deps: {
|
|
71
|
+
loadSnapshotInputs: () => loadSnapshotInputsForWorkspaceHeartbeat(readModels, {
|
|
72
|
+
state: options.state,
|
|
73
|
+
workspaceRoot: options.workspaceRoot,
|
|
74
|
+
}),
|
|
75
|
+
// T1.2.4: pass quietWorkflow dep so runSourceBackedQuiet can persist artifacts.
|
|
76
|
+
quietWorkflow: quietEnabled
|
|
77
|
+
? { workspaceRoot: options.workspaceRoot }
|
|
78
|
+
: undefined,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (options.runtimeRecorder) {
|
|
82
|
+
try {
|
|
83
|
+
await options.runtimeRecorder.recordHeartbeatCycle({ cycle, signal });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// T1.2.3: recorder must never break the heartbeat surface response.
|
|
87
|
+
// Failure here means status simply remains at its previous aggregate; the
|
|
88
|
+
// cycle outcome itself is still returned to the caller.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return cycle;
|
|
92
|
+
};
|
|
26
93
|
}
|
|
@@ -25,5 +25,11 @@ export interface CliReadModelsDeps {
|
|
|
25
25
|
observabilityDb: ObservabilityDatabase;
|
|
26
26
|
/** When set, explain can resolve delivery/fallback/report/source_ref and enrich decision subjects from lived-experience audit envelopes (T5.3.1 / T1.2.1). */
|
|
27
27
|
livedExperienceAuditStore?: AppendOnlyAuditStore;
|
|
28
|
+
/**
|
|
29
|
+
* T1.2.4: when set, `loadQuiet` and `loadDailyReport` also scan `.second-nature/quiet/{day}/`
|
|
30
|
+
* for persisted Quiet artifact JSON files (from `persistQuietArtifactToWorkspace`) and merge
|
|
31
|
+
* them into the read model so operators see non-zero counts after Quiet actually runs.
|
|
32
|
+
*/
|
|
33
|
+
workspaceRoot?: string;
|
|
28
34
|
}
|
|
29
35
|
export declare function createCliReadModels(deps: CliReadModelsDeps): CliReadModels;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { desc } from "drizzle-orm";
|
|
2
4
|
import { createQuietInputLoader } from "../../storage/services/quiet-input-loader.js";
|
|
3
5
|
import { AssetRepository } from "../../storage/repositories/asset-repository.js";
|
|
4
6
|
import { CredentialRepository } from "../../storage/repositories/credential-repository.js";
|
|
5
7
|
import { EvidenceQueryEngine } from "../../observability/query/evidence-query-engine.js";
|
|
6
|
-
import { decisionLedger, executionAttempts } from "../../observability/db/schema/index.js";
|
|
7
|
-
import {
|
|
8
|
+
import { decisionLedger, executionAttempts, } from "../../observability/db/schema/index.js";
|
|
9
|
+
import { AppendOnlyAuditStore } from "../../observability/audit/append-only-audit-store.js";
|
|
10
|
+
import { queryExplain, } from "../../observability/query/explain-query.js";
|
|
8
11
|
import { mapOperatorExplainToReadModel } from "./operator-explain-map.js";
|
|
9
|
-
import { loadOperatorFallbackRow, toOperatorFallbackView } from "../../storage/fallback/load-operator-fallback.js";
|
|
12
|
+
import { loadOperatorFallbackRow, toOperatorFallbackView, } from "../../storage/fallback/load-operator-fallback.js";
|
|
10
13
|
const INTERNAL_RUNTIME_PLATFORM_ID = "second-nature-runtime";
|
|
11
14
|
const INTERNAL_RUNTIME_TRACE_PREFIX = "sn-runtime-";
|
|
12
15
|
function toExplainQuery(subject) {
|
|
@@ -14,7 +17,9 @@ function toExplainQuery(subject) {
|
|
|
14
17
|
case "decision":
|
|
15
18
|
return { kind: "decision", decisionId: subject.id };
|
|
16
19
|
case "fallback": {
|
|
17
|
-
const ref = subject.id.startsWith("fallback:")
|
|
20
|
+
const ref = subject.id.startsWith("fallback:")
|
|
21
|
+
? subject.id
|
|
22
|
+
: `fallback:${subject.id}`;
|
|
18
23
|
return { kind: "fallback", fallbackRef: ref };
|
|
19
24
|
}
|
|
20
25
|
case "probe":
|
|
@@ -29,7 +34,11 @@ function toExplainQuery(subject) {
|
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
function isAuditOnlySubjectKind(kind) {
|
|
32
|
-
return kind === "fallback" ||
|
|
37
|
+
return (kind === "fallback" ||
|
|
38
|
+
kind === "probe" ||
|
|
39
|
+
kind === "report" ||
|
|
40
|
+
kind === "delivery" ||
|
|
41
|
+
kind === "source_ref");
|
|
33
42
|
}
|
|
34
43
|
function buildCredentialNextStep(status) {
|
|
35
44
|
if (status === "pending_verification")
|
|
@@ -38,6 +47,47 @@ function buildCredentialNextStep(status) {
|
|
|
38
47
|
return "refresh_credential_context";
|
|
39
48
|
return undefined;
|
|
40
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* T1.2.4: count persisted Quiet artifact JSON files under `.second-nature/quiet/{day}/`
|
|
52
|
+
* so `loadQuiet` / `loadDailyReport` can reflect Quiet artifacts in the read model.
|
|
53
|
+
*/
|
|
54
|
+
function countQuietArtifactsForDay(workspaceRoot, day) {
|
|
55
|
+
try {
|
|
56
|
+
const dir = path.join(workspaceRoot, ".second-nature", "quiet", day);
|
|
57
|
+
if (!fs.existsSync(dir))
|
|
58
|
+
return 0;
|
|
59
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(".json")).length;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* T1.2.4: scan the last N days under `.second-nature/quiet/` and count total JSON artifacts.
|
|
67
|
+
* Returns { totalArtifacts, recentDays } for merging into QuietReadModel.
|
|
68
|
+
*/
|
|
69
|
+
function countRecentQuietArtifacts(workspaceRoot, windowDays = 2) {
|
|
70
|
+
try {
|
|
71
|
+
const quietRoot = path.join(workspaceRoot, ".second-nature", "quiet");
|
|
72
|
+
if (!fs.existsSync(quietRoot))
|
|
73
|
+
return { totalArtifacts: 0, recentDays: [] };
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const recentDays = [];
|
|
76
|
+
let total = 0;
|
|
77
|
+
for (let i = 0; i < windowDays; i++) {
|
|
78
|
+
const d = new Date(now - i * 86400000).toISOString().slice(0, 10);
|
|
79
|
+
const count = countQuietArtifactsForDay(workspaceRoot, d);
|
|
80
|
+
if (count > 0) {
|
|
81
|
+
recentDays.push(d);
|
|
82
|
+
total += count;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { totalArtifacts: total, recentDays };
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return { totalArtifacts: 0, recentDays: [] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
41
91
|
function mapRuntimeStatus(attempt) {
|
|
42
92
|
if (!attempt) {
|
|
43
93
|
return "unknown";
|
|
@@ -61,7 +111,11 @@ export function createCliReadModels(deps) {
|
|
|
61
111
|
const credentialRepository = new CredentialRepository(deps.stateDb);
|
|
62
112
|
const quietLoader = createQuietInputLoader(assetRepository);
|
|
63
113
|
const evidenceQuery = new EvidenceQueryEngine(deps.observabilityDb);
|
|
64
|
-
|
|
114
|
+
// T1.2.5 (CH-14-05): default-inject an empty AppendOnlyAuditStore so `explain` does not
|
|
115
|
+
// immediately return `lived_experience_audit_store_unavailable` for callers that don't supply
|
|
116
|
+
// an explicit store. The empty store means audit-only subjects return `no_matching_audit_events`
|
|
117
|
+
// instead of a configuration error — which is more accurate and less alarming to operators.
|
|
118
|
+
const auditStore = deps.livedExperienceAuditStore ?? new AppendOnlyAuditStore();
|
|
65
119
|
return {
|
|
66
120
|
async loadStatus(_scope) {
|
|
67
121
|
let recentAttempts = [];
|
|
@@ -94,15 +148,26 @@ export function createCliReadModels(deps) {
|
|
|
94
148
|
credentials = [];
|
|
95
149
|
}
|
|
96
150
|
const latestRuntimeAttempt = recentAttempts.find((attempt) => attempt.platformId === INTERNAL_RUNTIME_PLATFORM_ID);
|
|
151
|
+
// CH-15-04 (CH-14-03): latestConnectorAttempt is the most recent execution attempt whose
|
|
152
|
+
// platformId is NOT the internal sn-runtime sentinel — i.e. a real connector platform
|
|
153
|
+
// (Moltbook, EvoMap, etc.). The `connectors` array in StatusReadModel reflects this single
|
|
154
|
+
// most-recent non-runtime attempt, NOT the full connector manifest. An empty array means
|
|
155
|
+
// no connector attempt has been recorded yet, not that connectors are misconfigured.
|
|
97
156
|
const latestConnectorAttempt = recentAttempts.find((attempt) => attempt.platformId !== INTERNAL_RUNTIME_PLATFORM_ID);
|
|
98
157
|
const latestRuntimeDecision = recentDecisions.find((decision) => decision.traceId.startsWith(INTERNAL_RUNTIME_TRACE_PREFIX));
|
|
99
|
-
const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ??
|
|
158
|
+
const runtimeUpdatedAt = latestRuntimeAttempt?.finishedAt ??
|
|
159
|
+
latestRuntimeAttempt?.startedAt ??
|
|
160
|
+
latestRuntimeDecision?.createdAt ??
|
|
161
|
+
"";
|
|
100
162
|
const quietMode = latestRuntimeDecision?.mode === "quiet" ||
|
|
101
163
|
latestRuntimeDecision?.mode === "maintenance_only" ||
|
|
102
164
|
latestRuntimeDecision?.mode === "paused_for_interrupt"
|
|
103
165
|
? latestRuntimeDecision.mode
|
|
104
166
|
: "unknown";
|
|
105
|
-
const riskFlags = [
|
|
167
|
+
const riskFlags = [
|
|
168
|
+
latestRuntimeAttempt?.failureClass,
|
|
169
|
+
latestConnectorAttempt?.failureClass,
|
|
170
|
+
].filter((value) => Boolean(value));
|
|
106
171
|
const connectorSummary = latestConnectorAttempt
|
|
107
172
|
? [
|
|
108
173
|
{
|
|
@@ -126,11 +191,14 @@ export function createCliReadModels(deps) {
|
|
|
126
191
|
quiet: {
|
|
127
192
|
mode: quietMode,
|
|
128
193
|
lastEvent: latestRuntimeDecision?.traceId,
|
|
129
|
-
interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt"
|
|
194
|
+
interrupted: latestRuntimeDecision?.mode === "paused_for_interrupt"
|
|
195
|
+
? true
|
|
196
|
+
: undefined,
|
|
130
197
|
},
|
|
131
198
|
connectors: connectorSummary,
|
|
132
199
|
credentials: credentials.map((item) => ({
|
|
133
|
-
platformId: item.platformId ??
|
|
200
|
+
platformId: item.platformId ??
|
|
201
|
+
item.platform_id,
|
|
134
202
|
status: item.status,
|
|
135
203
|
nextStep: buildCredentialNextStep(item.status),
|
|
136
204
|
})),
|
|
@@ -138,25 +206,49 @@ export function createCliReadModels(deps) {
|
|
|
138
206
|
level: riskFlags.length > 0 ? "medium" : "low",
|
|
139
207
|
flags: riskFlags,
|
|
140
208
|
},
|
|
209
|
+
// T1.2.5 (CH-14-04): default delivery posture is workspace_default_none because the
|
|
210
|
+
// workspace heartbeat hardcodes `deliveryCapability: { target: "none" }` until a host
|
|
211
|
+
// capability probe explicitly sets a valid target.
|
|
212
|
+
deliveryPosture: {
|
|
213
|
+
verdict: "none",
|
|
214
|
+
source: "workspace_default_none",
|
|
215
|
+
reasonCode: "delivery_target_none",
|
|
216
|
+
},
|
|
141
217
|
};
|
|
142
218
|
},
|
|
143
219
|
async loadDailyReport(day) {
|
|
144
220
|
let bundle;
|
|
145
221
|
try {
|
|
146
222
|
bundle = await quietLoader.loadQuietInputs({
|
|
147
|
-
dateRange: {
|
|
148
|
-
|
|
223
|
+
dateRange: {
|
|
224
|
+
start: `${day}T00:00:00.000Z`,
|
|
225
|
+
end: `${day}T23:59:59.999Z`,
|
|
226
|
+
},
|
|
227
|
+
assetFilters: {
|
|
228
|
+
includeJournal: false,
|
|
229
|
+
includeReports: true,
|
|
230
|
+
includeCurated: false,
|
|
231
|
+
},
|
|
149
232
|
});
|
|
150
233
|
}
|
|
151
234
|
catch {
|
|
152
235
|
bundle = { dailyReports: [], journalEntries: [], sourceCount: 0 };
|
|
153
236
|
}
|
|
237
|
+
// T1.2.4: merge persisted Quiet artifact JSON files from `.second-nature/quiet/{day}/`
|
|
238
|
+
// into the daily report sourceRefs so the read model reflects artifacts written by
|
|
239
|
+
// `persistQuietArtifactToWorkspace` (closes the canonical read/write gap for loadDailyReport).
|
|
240
|
+
const fsArtifactCount = deps.workspaceRoot
|
|
241
|
+
? countQuietArtifactsForDay(deps.workspaceRoot, day)
|
|
242
|
+
: 0;
|
|
154
243
|
const report = bundle.dailyReports[0];
|
|
244
|
+
const existingSources = report?.sources ?? [];
|
|
245
|
+
// Append synthetic source refs for each FS artifact not already in the list.
|
|
246
|
+
const fsSourceRefs = Array.from({ length: fsArtifactCount }, (_, i) => `quiet_artifact:${day}:${i}`).filter((ref) => !existingSources.includes(ref));
|
|
155
247
|
return {
|
|
156
248
|
day,
|
|
157
249
|
summary: report?.summary ?? "",
|
|
158
250
|
highlights: report?.highlights ?? [],
|
|
159
|
-
sourceRefs:
|
|
251
|
+
sourceRefs: [...existingSources, ...fsSourceRefs],
|
|
160
252
|
};
|
|
161
253
|
},
|
|
162
254
|
async loadQuiet(scope) {
|
|
@@ -172,11 +264,20 @@ export function createCliReadModels(deps) {
|
|
|
172
264
|
catch {
|
|
173
265
|
bundle = { dailyReports: [], journalEntries: [], sourceCount: 0 };
|
|
174
266
|
}
|
|
267
|
+
// T1.2.4 (CH-14-07): also count persisted Quiet artifact JSON files under
|
|
268
|
+
// `.second-nature/quiet/` so that once `runSourceBackedQuiet` has written
|
|
269
|
+
// artifacts to disk, the read model is non-zero even if the legacy memory/
|
|
270
|
+
// journal path is empty.
|
|
271
|
+
const quietArtifacts = deps.workspaceRoot
|
|
272
|
+
? countRecentQuietArtifacts(deps.workspaceRoot, 2)
|
|
273
|
+
: { totalArtifacts: 0, recentDays: [] };
|
|
274
|
+
const totalSourceCount = bundle.sourceCount + quietArtifacts.totalArtifacts;
|
|
275
|
+
const totalReportCount = bundle.dailyReports.length + quietArtifacts.totalArtifacts;
|
|
175
276
|
return {
|
|
176
277
|
scope,
|
|
177
|
-
mode:
|
|
178
|
-
sourceCount:
|
|
179
|
-
reportCount:
|
|
278
|
+
mode: totalSourceCount > 0 ? "quiet" : "unknown",
|
|
279
|
+
sourceCount: totalSourceCount,
|
|
280
|
+
reportCount: totalReportCount,
|
|
180
281
|
recentJournalCount: bundle.journalEntries.length,
|
|
181
282
|
};
|
|
182
283
|
},
|
|
@@ -209,7 +310,8 @@ export function createCliReadModels(deps) {
|
|
|
209
310
|
};
|
|
210
311
|
}
|
|
211
312
|
return {
|
|
212
|
-
platformId: record.platformId ??
|
|
313
|
+
platformId: record.platformId ??
|
|
314
|
+
record.platform_id,
|
|
213
315
|
status: record.status,
|
|
214
316
|
verificationDeadline: record.expiresAt ?? undefined,
|
|
215
317
|
attemptsRemaining: record.attemptsRemaining ?? undefined,
|
|
@@ -224,6 +326,9 @@ export function createCliReadModels(deps) {
|
|
|
224
326
|
},
|
|
225
327
|
async explain(subject) {
|
|
226
328
|
const q = toExplainQuery(subject);
|
|
329
|
+
// T1.2.5: auditStore is always non-null (default-injected), so the explain path always
|
|
330
|
+
// has a store available. For audit-only subjects with no matching events the summary
|
|
331
|
+
// from queryExplain will be "no_matching_audit_events" — accurate and non-alarming.
|
|
227
332
|
if (auditStore && q) {
|
|
228
333
|
const op = queryExplain(q, auditStore);
|
|
229
334
|
if (isAuditOnlySubjectKind(subject.kind)) {
|
|
@@ -236,12 +341,16 @@ export function createCliReadModels(deps) {
|
|
|
236
341
|
if (isAuditOnlySubjectKind(subject.kind)) {
|
|
237
342
|
return {
|
|
238
343
|
subjectType: subject.kind,
|
|
239
|
-
|
|
240
|
-
|
|
344
|
+
// auditStore is always present (default-injected by T1.2.5), so this branch is
|
|
345
|
+
// only reached when q is undefined (unresolvable subject kind).
|
|
346
|
+
conclusion: "no_matching_audit_events",
|
|
347
|
+
keyFactors: [],
|
|
241
348
|
evidenceRefs: [],
|
|
242
349
|
};
|
|
243
350
|
}
|
|
244
|
-
const query = subject.kind === "decision" ||
|
|
351
|
+
const query = subject.kind === "decision" ||
|
|
352
|
+
subject.kind === "platform-selection" ||
|
|
353
|
+
subject.kind === "outreach"
|
|
245
354
|
? { decisionId: subject.id }
|
|
246
355
|
: { assetId: subject.id };
|
|
247
356
|
const bundle = await evidenceQuery.queryEvidence(query);
|
|
@@ -27,6 +27,34 @@ export interface RiskSummary {
|
|
|
27
27
|
level: "low" | "medium" | "high";
|
|
28
28
|
flags: string[];
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* T1.2.5 (CH-14-04): delivery posture summarises why `deliveryCapability.target` is `none`
|
|
32
|
+
* and which layer set it — workspace heartbeat default vs OpenClaw cron config vs host probe.
|
|
33
|
+
*
|
|
34
|
+
* CH-15-02 implementation note: `loadStatus` currently always emits `workspace_default_none`
|
|
35
|
+
* because the workspace heartbeat hardcodes `target: none` and no T1.1.2 HostCapabilityReport
|
|
36
|
+
* probe is wired into the read model path yet. The other two `source` values are reserved for
|
|
37
|
+
* future integration:
|
|
38
|
+
* - `openclaw_cron_delivery_none`: when the OpenClaw cron layer exposes `delivery.mode: none`
|
|
39
|
+
* in the host config and that value is surfaced via a new probe or bridge field.
|
|
40
|
+
* - `host_capability_probe`: when `HostCapabilityReport.deliveryTarget` is read from the DB
|
|
41
|
+
* (T1.1.2) and routed into `loadStatus`.
|
|
42
|
+
* Do NOT infer either value without a real observation — see ADR-007 "no proof, not sent".
|
|
43
|
+
*/
|
|
44
|
+
export interface DeliveryPosture {
|
|
45
|
+
/** Current effective verdict: none = no delivery channel; available = a valid target exists. */
|
|
46
|
+
verdict: "none" | "available";
|
|
47
|
+
/**
|
|
48
|
+
* Stable source discriminator for operator tooling (CH-15-02: only workspace_default_none
|
|
49
|
+
* is emitted today; cron/probe values require additional host-side wiring):
|
|
50
|
+
* workspace_default_none — workspace heartbeat hardcodes target:none (no host probe ran).
|
|
51
|
+
* openclaw_cron_delivery_none — cron layer has delivery.mode:none (host config decision).
|
|
52
|
+
* host_capability_probe — a HostCapabilityReport probe determined the posture.
|
|
53
|
+
*/
|
|
54
|
+
source: "workspace_default_none" | "openclaw_cron_delivery_none" | "host_capability_probe";
|
|
55
|
+
/** Human-readable reason code included in explain surfaces. */
|
|
56
|
+
reasonCode: string;
|
|
57
|
+
}
|
|
30
58
|
export interface StatusReadModel {
|
|
31
59
|
runtime: RuntimeSummary;
|
|
32
60
|
rhythm: RhythmSummary;
|
|
@@ -34,6 +62,11 @@ export interface StatusReadModel {
|
|
|
34
62
|
connectors: ConnectorSummary[];
|
|
35
63
|
credentials: CredentialSummary[];
|
|
36
64
|
risk: RiskSummary;
|
|
65
|
+
/**
|
|
66
|
+
* T1.2.5: structured delivery posture so operators can distinguish workspace default "none"
|
|
67
|
+
* from cron-layer "none" without inspecting raw heartbeat JSON.
|
|
68
|
+
*/
|
|
69
|
+
deliveryPosture?: DeliveryPosture;
|
|
37
70
|
}
|
|
38
71
|
export interface DailyReportReadModel {
|
|
39
72
|
day: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { buildContinuitySnapshot } from "./snapshot-builder.js";
|
|
2
|
-
import { buildHeartbeatRuntimeSnapshot } from "./runtime-snapshot.js";
|
|
1
|
+
import { buildContinuitySnapshot, } from "./snapshot-builder.js";
|
|
2
|
+
import { buildHeartbeatRuntimeSnapshot, } from "./runtime-snapshot.js";
|
|
3
3
|
import { planCandidateIntents } from "../orchestrator/intent-planner.js";
|
|
4
4
|
import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
|
|
5
|
-
import { dispatchUserOutreachIntent } from "../outreach/dispatch-user-outreach.js";
|
|
5
|
+
import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
|
|
6
6
|
import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
|
|
7
7
|
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
8
8
|
/**
|
|
@@ -10,7 +10,9 @@ import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
|
10
10
|
* Exported for unit tests (CR-M1 wiring).
|
|
11
11
|
*/
|
|
12
12
|
export async function resolveAllowedIntentResult(intent, runtime, inputs, signal, deps) {
|
|
13
|
-
const day = typeof signal.payload.timestamp === "string"
|
|
13
|
+
const day = typeof signal.payload.timestamp === "string"
|
|
14
|
+
? signal.payload.timestamp.slice(0, 10)
|
|
15
|
+
: "1970-01-01";
|
|
14
16
|
if (intent.effectClass === "user_outreach" && deps.outreachDispatch) {
|
|
15
17
|
return dispatchUserOutreachIntent({
|
|
16
18
|
candidate: intent,
|
|
@@ -22,7 +24,9 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
if (deps.quietWorkflow &&
|
|
25
|
-
(intent.kind === "quiet" ||
|
|
27
|
+
(intent.kind === "quiet" ||
|
|
28
|
+
(intent.kind === "reflection" &&
|
|
29
|
+
intent.effectClass === "narrative_reflection"))) {
|
|
26
30
|
const quietRun = await runSourceBackedQuiet({
|
|
27
31
|
candidate: intent,
|
|
28
32
|
runtime,
|
|
@@ -32,11 +36,28 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
32
36
|
});
|
|
33
37
|
return quietRun.result;
|
|
34
38
|
}
|
|
39
|
+
// T2.2.3 (CH-14-02/03 / CH-15-01): all intent_selected results must carry at least one
|
|
40
|
+
// machine-readable reason so operators can distinguish between effect classes:
|
|
41
|
+
// - maintenance / no_effect → "internal_tick" (no external side-effects)
|
|
42
|
+
// - connector_action without dispatch wired → "connector_dispatch_unwired"
|
|
43
|
+
// - external_platform_action / memory_curation → not generated by intent-planner today;
|
|
44
|
+
// if a future path produces them, they will reach the fallback [] branch below and
|
|
45
|
+
// should have dedicated reason codes added (e.g. "external_platform_action_unwired").
|
|
46
|
+
// - other (outreach / quiet) → caught by the early-return branches above
|
|
47
|
+
const noExternalEffect = intent.effectClass === "maintenance" ||
|
|
48
|
+
intent.effectClass === "no_effect" ||
|
|
49
|
+
intent.kind === "maintenance";
|
|
50
|
+
const connectorUnwired = intent.effectClass === "connector_action";
|
|
51
|
+
const reasons = noExternalEffect
|
|
52
|
+
? ["internal_tick"]
|
|
53
|
+
: connectorUnwired
|
|
54
|
+
? ["connector_dispatch_unwired"]
|
|
55
|
+
: [];
|
|
35
56
|
return {
|
|
36
57
|
scope: "rhythm",
|
|
37
58
|
status: "intent_selected",
|
|
38
59
|
selectedIntentId: intent.id,
|
|
39
|
-
reasons
|
|
60
|
+
reasons,
|
|
40
61
|
};
|
|
41
62
|
}
|
|
42
63
|
/**
|
|
@@ -82,7 +103,9 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
82
103
|
reasons: evaluation.reasons,
|
|
83
104
|
};
|
|
84
105
|
const resolved = await resolveAllowedIntentResult(intent, runtime, inputs, signal, deps);
|
|
85
|
-
const result = resolved.status === "intent_selected" &&
|
|
106
|
+
const result = resolved.status === "intent_selected" &&
|
|
107
|
+
resolved.reasons.length === 0 &&
|
|
108
|
+
evaluation.reasons.length > 0
|
|
86
109
|
? { ...resolved, reasons: evaluation.reasons }
|
|
87
110
|
: resolved;
|
|
88
111
|
await emitTrace(result);
|
|
@@ -27,7 +27,7 @@ export function startRuntimeService(ctx) {
|
|
|
27
27
|
// - control-plane-system (heartbeat bridge preparation)
|
|
28
28
|
const workspaceRoot = ctx?.workspaceRoot ?? process.cwd();
|
|
29
29
|
/** Keep in sync with `plugin/package.json` when cutting releases. */
|
|
30
|
-
const version = "0.1.
|
|
30
|
+
const version = "0.1.19";
|
|
31
31
|
activeHandle = {
|
|
32
32
|
ready: true,
|
|
33
33
|
version,
|
|
@@ -6,83 +6,83 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import * as schema from "./schema/index.js";
|
|
7
7
|
// Pre-initialize sql.js WASM at module load time
|
|
8
8
|
const SQL = await initSqlJs();
|
|
9
|
-
const OBSERVABILITY_SCHEMA_SQL = `
|
|
10
|
-
CREATE TABLE IF NOT EXISTS decision_ledger (
|
|
11
|
-
id TEXT PRIMARY KEY,
|
|
12
|
-
tick_id TEXT NOT NULL,
|
|
13
|
-
trace_id TEXT NOT NULL,
|
|
14
|
-
intent_id TEXT,
|
|
15
|
-
platform_id TEXT,
|
|
16
|
-
verdict TEXT NOT NULL,
|
|
17
|
-
mode TEXT NOT NULL,
|
|
18
|
-
reasons TEXT NOT NULL,
|
|
19
|
-
reason_codes TEXT NOT NULL,
|
|
20
|
-
decision_basis TEXT NOT NULL,
|
|
21
|
-
evidence_refs TEXT NOT NULL,
|
|
22
|
-
model_eval_ref TEXT,
|
|
23
|
-
created_at TEXT NOT NULL
|
|
24
|
-
);
|
|
25
|
-
CREATE UNIQUE INDEX IF NOT EXISTS decision_trace_idx ON decision_ledger(trace_id);
|
|
26
|
-
CREATE INDEX IF NOT EXISTS decision_tick_idx ON decision_ledger(tick_id);
|
|
27
|
-
CREATE TABLE IF NOT EXISTS execution_attempts (
|
|
28
|
-
id TEXT PRIMARY KEY,
|
|
29
|
-
trace_id TEXT NOT NULL,
|
|
30
|
-
decision_id TEXT NOT NULL,
|
|
31
|
-
intent_id TEXT NOT NULL,
|
|
32
|
-
platform_id TEXT NOT NULL,
|
|
33
|
-
capability TEXT NOT NULL,
|
|
34
|
-
channel TEXT NOT NULL,
|
|
35
|
-
status TEXT NOT NULL,
|
|
36
|
-
commit_state TEXT,
|
|
37
|
-
failure_class TEXT,
|
|
38
|
-
retry_policy TEXT,
|
|
39
|
-
idempotency_key TEXT,
|
|
40
|
-
started_at TEXT,
|
|
41
|
-
finished_at TEXT
|
|
42
|
-
);
|
|
43
|
-
CREATE UNIQUE INDEX IF NOT EXISTS attempt_trace_idx ON execution_attempts(trace_id);
|
|
44
|
-
CREATE INDEX IF NOT EXISTS attempt_decision_idx ON execution_attempts(decision_id);
|
|
45
|
-
CREATE INDEX IF NOT EXISTS attempt_platform_idx ON execution_attempts(platform_id);
|
|
46
|
-
CREATE TABLE IF NOT EXISTS governance_audit (
|
|
47
|
-
id TEXT PRIMARY KEY,
|
|
48
|
-
event_type TEXT NOT NULL,
|
|
49
|
-
proposal_id TEXT,
|
|
50
|
-
target_asset_id TEXT,
|
|
51
|
-
asset_path TEXT,
|
|
52
|
-
status_from TEXT,
|
|
53
|
-
status_to TEXT NOT NULL,
|
|
54
|
-
before_hash TEXT,
|
|
55
|
-
after_hash TEXT,
|
|
56
|
-
supporting_sources TEXT,
|
|
57
|
-
reason TEXT,
|
|
58
|
-
verification_deadline TEXT,
|
|
59
|
-
attempts_remaining INTEGER,
|
|
60
|
-
created_at TEXT NOT NULL
|
|
61
|
-
);
|
|
62
|
-
CREATE INDEX IF NOT EXISTS audit_proposal_idx ON governance_audit(proposal_id);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS audit_asset_idx ON governance_audit(target_asset_id);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS audit_event_idx ON governance_audit(event_type);
|
|
65
|
-
CREATE TABLE IF NOT EXISTS redaction_manifest (
|
|
66
|
-
id TEXT PRIMARY KEY,
|
|
67
|
-
event_id TEXT NOT NULL,
|
|
68
|
-
event_type TEXT NOT NULL,
|
|
69
|
-
field_name TEXT NOT NULL,
|
|
70
|
-
action TEXT NOT NULL,
|
|
71
|
-
original_value_hash TEXT,
|
|
72
|
-
created_at TEXT NOT NULL
|
|
73
|
-
);
|
|
74
|
-
CREATE INDEX IF NOT EXISTS redact_event_idx ON redaction_manifest(event_id);
|
|
75
|
-
CREATE TABLE IF NOT EXISTS host_capability_reports (
|
|
76
|
-
report_id TEXT PRIMARY KEY,
|
|
77
|
-
generated_at TEXT NOT NULL,
|
|
78
|
-
host_version TEXT,
|
|
79
|
-
observed_version TEXT,
|
|
80
|
-
doc_checked_at TEXT NOT NULL,
|
|
81
|
-
doc_links_json TEXT NOT NULL,
|
|
82
|
-
delivery_target TEXT NOT NULL,
|
|
83
|
-
conflict_records_json TEXT NOT NULL,
|
|
84
|
-
full_report_json TEXT NOT NULL
|
|
85
|
-
);
|
|
9
|
+
const OBSERVABILITY_SCHEMA_SQL = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS decision_ledger (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
tick_id TEXT NOT NULL,
|
|
13
|
+
trace_id TEXT NOT NULL,
|
|
14
|
+
intent_id TEXT,
|
|
15
|
+
platform_id TEXT,
|
|
16
|
+
verdict TEXT NOT NULL,
|
|
17
|
+
mode TEXT NOT NULL,
|
|
18
|
+
reasons TEXT NOT NULL,
|
|
19
|
+
reason_codes TEXT NOT NULL,
|
|
20
|
+
decision_basis TEXT NOT NULL,
|
|
21
|
+
evidence_refs TEXT NOT NULL,
|
|
22
|
+
model_eval_ref TEXT,
|
|
23
|
+
created_at TEXT NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE UNIQUE INDEX IF NOT EXISTS decision_trace_idx ON decision_ledger(trace_id);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS decision_tick_idx ON decision_ledger(tick_id);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS execution_attempts (
|
|
28
|
+
id TEXT PRIMARY KEY,
|
|
29
|
+
trace_id TEXT NOT NULL,
|
|
30
|
+
decision_id TEXT NOT NULL,
|
|
31
|
+
intent_id TEXT NOT NULL,
|
|
32
|
+
platform_id TEXT NOT NULL,
|
|
33
|
+
capability TEXT NOT NULL,
|
|
34
|
+
channel TEXT NOT NULL,
|
|
35
|
+
status TEXT NOT NULL,
|
|
36
|
+
commit_state TEXT,
|
|
37
|
+
failure_class TEXT,
|
|
38
|
+
retry_policy TEXT,
|
|
39
|
+
idempotency_key TEXT,
|
|
40
|
+
started_at TEXT,
|
|
41
|
+
finished_at TEXT
|
|
42
|
+
);
|
|
43
|
+
CREATE UNIQUE INDEX IF NOT EXISTS attempt_trace_idx ON execution_attempts(trace_id);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS attempt_decision_idx ON execution_attempts(decision_id);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS attempt_platform_idx ON execution_attempts(platform_id);
|
|
46
|
+
CREATE TABLE IF NOT EXISTS governance_audit (
|
|
47
|
+
id TEXT PRIMARY KEY,
|
|
48
|
+
event_type TEXT NOT NULL,
|
|
49
|
+
proposal_id TEXT,
|
|
50
|
+
target_asset_id TEXT,
|
|
51
|
+
asset_path TEXT,
|
|
52
|
+
status_from TEXT,
|
|
53
|
+
status_to TEXT NOT NULL,
|
|
54
|
+
before_hash TEXT,
|
|
55
|
+
after_hash TEXT,
|
|
56
|
+
supporting_sources TEXT,
|
|
57
|
+
reason TEXT,
|
|
58
|
+
verification_deadline TEXT,
|
|
59
|
+
attempts_remaining INTEGER,
|
|
60
|
+
created_at TEXT NOT NULL
|
|
61
|
+
);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS audit_proposal_idx ON governance_audit(proposal_id);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS audit_asset_idx ON governance_audit(target_asset_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS audit_event_idx ON governance_audit(event_type);
|
|
65
|
+
CREATE TABLE IF NOT EXISTS redaction_manifest (
|
|
66
|
+
id TEXT PRIMARY KEY,
|
|
67
|
+
event_id TEXT NOT NULL,
|
|
68
|
+
event_type TEXT NOT NULL,
|
|
69
|
+
field_name TEXT NOT NULL,
|
|
70
|
+
action TEXT NOT NULL,
|
|
71
|
+
original_value_hash TEXT,
|
|
72
|
+
created_at TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS redact_event_idx ON redaction_manifest(event_id);
|
|
75
|
+
CREATE TABLE IF NOT EXISTS host_capability_reports (
|
|
76
|
+
report_id TEXT PRIMARY KEY,
|
|
77
|
+
generated_at TEXT NOT NULL,
|
|
78
|
+
host_version TEXT,
|
|
79
|
+
observed_version TEXT,
|
|
80
|
+
doc_checked_at TEXT NOT NULL,
|
|
81
|
+
doc_links_json TEXT NOT NULL,
|
|
82
|
+
delivery_target TEXT NOT NULL,
|
|
83
|
+
conflict_records_json TEXT NOT NULL,
|
|
84
|
+
full_report_json TEXT NOT NULL
|
|
85
|
+
);
|
|
86
86
|
`;
|
|
87
87
|
function resolveDbPath(filename) {
|
|
88
88
|
if (path.isAbsolute(filename) || filename === ":memory:") {
|
|
@@ -8,6 +8,7 @@ export { redactEvent, createEmptyManifest, mergeManifests, type RedactionManifes
|
|
|
8
8
|
export { DecisionLedger, type QuietLifecycleEvent, type OutreachDecision, type HeartbeatDecisionEvent } from "./services/decision-ledger.js";
|
|
9
9
|
export { GovernanceAudit, type CredentialLifecycleAudit } from "./services/governance-audit.js";
|
|
10
10
|
export { ExecutionTelemetry, type ExecutionAttemptInput } from "./services/execution-telemetry.js";
|
|
11
|
+
export { createRuntimeDecisionRecorder, RUNTIME_DECISION_TRACE_PREFIX, RUNTIME_INTERNAL_PLATFORM_ID, type RuntimeDecisionRecorder, type RecordHeartbeatCycleInput, type RecordHeartbeatCycleOutput, } from "./services/runtime-decision-recorder.js";
|
|
11
12
|
export { LivedExperienceAuditRecorder, createLivedExperienceAuditRecorder, type DecisionTracePayload, type DeliveryAuditPayload, type DeliveryAuditStatus, type ExplainLinkageSummary, type GuidanceGroundingAuditPayload, type SourceCoverageAuditPayload, } from "./services/lived-experience-audit.js";
|
|
12
13
|
export { GovernancePlaneRecorder, createGovernancePlaneRecorder, type AuditAppendAck, type ConnectorAttemptAudit, type ConnectorAttemptOutcome, type StateGovernanceAudit, type StateGovernanceKind, } from "./services/governance-plane-recorder.js";
|
|
13
14
|
export { queryExplain, type ExplainQuery, type OperatorExplainReadModel, type RedactedExplainEvent, } from "./query/explain-query.js";
|
|
@@ -8,6 +8,7 @@ export { redactEvent, createEmptyManifest, mergeManifests, } from "./redaction/m
|
|
|
8
8
|
export { DecisionLedger } from "./services/decision-ledger.js";
|
|
9
9
|
export { GovernanceAudit } from "./services/governance-audit.js";
|
|
10
10
|
export { ExecutionTelemetry } from "./services/execution-telemetry.js";
|
|
11
|
+
export { createRuntimeDecisionRecorder, RUNTIME_DECISION_TRACE_PREFIX, RUNTIME_INTERNAL_PLATFORM_ID, } from "./services/runtime-decision-recorder.js";
|
|
11
12
|
export { LivedExperienceAuditRecorder, createLivedExperienceAuditRecorder, } from "./services/lived-experience-audit.js";
|
|
12
13
|
export { GovernancePlaneRecorder, createGovernancePlaneRecorder, } from "./services/governance-plane-recorder.js";
|
|
13
14
|
export { queryExplain, } from "./query/explain-query.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ObservabilityDatabase } from "../db/index.js";
|
|
2
|
+
import { DecisionLedger, type HeartbeatDecisionEvent } from "./decision-ledger.js";
|
|
3
|
+
import { ExecutionTelemetry } from "./execution-telemetry.js";
|
|
4
|
+
import type { HeartbeatCycleResult, HeartbeatSignal } from "../../core/second-nature/heartbeat/signal.js";
|
|
5
|
+
export declare const RUNTIME_DECISION_TRACE_PREFIX = "sn-runtime-";
|
|
6
|
+
export declare const RUNTIME_INTERNAL_PLATFORM_ID = "second-nature-runtime";
|
|
7
|
+
export interface RecordHeartbeatCycleInput {
|
|
8
|
+
cycle: HeartbeatCycleResult;
|
|
9
|
+
signal: HeartbeatSignal;
|
|
10
|
+
/**
|
|
11
|
+
* Override rhythm `mode` written to the ledger row. When omitted, falls back
|
|
12
|
+
* to `"active"`; downstream loadStatus only treats `quiet` /
|
|
13
|
+
* `maintenance_only` / `paused_for_interrupt` as Quiet-aware values.
|
|
14
|
+
*/
|
|
15
|
+
rhythmMode?: HeartbeatDecisionEvent["mode"];
|
|
16
|
+
}
|
|
17
|
+
export interface RecordHeartbeatCycleOutput {
|
|
18
|
+
traceId: string;
|
|
19
|
+
decisionId: string;
|
|
20
|
+
attemptId: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RuntimeDecisionRecorder {
|
|
23
|
+
recordHeartbeatCycle(input: RecordHeartbeatCycleInput): Promise<RecordHeartbeatCycleOutput>;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateRuntimeDecisionRecorderDeps {
|
|
26
|
+
ledger?: DecisionLedger;
|
|
27
|
+
telemetry?: ExecutionTelemetry;
|
|
28
|
+
}
|
|
29
|
+
export declare function createRuntimeDecisionRecorder(observabilityDb: ObservabilityDatabase, overrides?: CreateRuntimeDecisionRecorderDeps): RuntimeDecisionRecorder;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime Decision Recorder (T1.2.3).
|
|
3
|
+
*
|
|
4
|
+
* Core logic: after a workspace `runHeartbeatCycle` completes, persist two rows that
|
|
5
|
+
* `loadStatus` already filters on, so operator status stops returning `unknown` for
|
|
6
|
+
* `rhythm.mode` / `runtime.serviceStatus` once the runtime has executed at least once.
|
|
7
|
+
* - `decision_ledger` row via `DecisionLedger.recordHeartbeatDecision()` with
|
|
8
|
+
* `traceId` prefix `sn-runtime-` (matches `INTERNAL_RUNTIME_TRACE_PREFIX`).
|
|
9
|
+
* - `execution_attempts` row via `ExecutionTelemetry.startAttempt` +
|
|
10
|
+
* `completeAttempt` with `platformId === "second-nature-runtime"` (matches
|
|
11
|
+
* `INTERNAL_RUNTIME_PLATFORM_ID`).
|
|
12
|
+
*
|
|
13
|
+
* Boundaries:
|
|
14
|
+
* - Recorder failure must NOT break the heartbeat surface response — caller wraps with try/catch.
|
|
15
|
+
* - Carrier-only / probe-only / runtime-unavailable paths do NOT invoke this recorder
|
|
16
|
+
* (their semantics intentionally remain "unknown" until a full-runtime turn happens).
|
|
17
|
+
* - This is a derived observability writer; it is not the canonical decision producer
|
|
18
|
+
* (control-plane keeps that contract). It exists to close the read-side aggregation gap.
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
import { DecisionLedger } from "./decision-ledger.js";
|
|
22
|
+
import { ExecutionTelemetry } from "./execution-telemetry.js";
|
|
23
|
+
export const RUNTIME_DECISION_TRACE_PREFIX = "sn-runtime-";
|
|
24
|
+
export const RUNTIME_INTERNAL_PLATFORM_ID = "second-nature-runtime";
|
|
25
|
+
const RUNTIME_INTERNAL_CAPABILITY = "runtime.heartbeat";
|
|
26
|
+
const RUNTIME_INTERNAL_CHANNEL = "internal";
|
|
27
|
+
export function createRuntimeDecisionRecorder(observabilityDb, overrides = {}) {
|
|
28
|
+
const ledger = overrides.ledger ?? new DecisionLedger(observabilityDb);
|
|
29
|
+
const telemetry = overrides.telemetry ?? new ExecutionTelemetry(observabilityDb);
|
|
30
|
+
return {
|
|
31
|
+
async recordHeartbeatCycle({ cycle, signal, rhythmMode }) {
|
|
32
|
+
const timestamp = typeof signal.payload.timestamp === "string" && signal.payload.timestamp.trim().length > 0
|
|
33
|
+
? signal.payload.timestamp
|
|
34
|
+
: new Date().toISOString();
|
|
35
|
+
const uniqueId = randomUUID();
|
|
36
|
+
const traceId = `${RUNTIME_DECISION_TRACE_PREFIX}${cycle.scope}-${cycle.status}-${uniqueId}`;
|
|
37
|
+
const decisionId = `decision-runtime-${uniqueId}`;
|
|
38
|
+
const tickId = `tick-runtime-${uniqueId}`;
|
|
39
|
+
const event = {
|
|
40
|
+
id: decisionId,
|
|
41
|
+
tickId,
|
|
42
|
+
traceId,
|
|
43
|
+
runtimeScope: cycle.scope,
|
|
44
|
+
triggerSource: signal.trigger,
|
|
45
|
+
decisionStatus: mapCycleStatus(cycle.status),
|
|
46
|
+
reasons: cycle.reasons,
|
|
47
|
+
intentId: cycle.selectedIntentId,
|
|
48
|
+
mode: rhythmMode ?? "active",
|
|
49
|
+
createdAt: timestamp,
|
|
50
|
+
};
|
|
51
|
+
await ledger.recordHeartbeatDecision(event);
|
|
52
|
+
const attemptId = await telemetry.startAttempt({
|
|
53
|
+
traceId,
|
|
54
|
+
decisionId,
|
|
55
|
+
intentId: cycle.selectedIntentId ?? `${RUNTIME_INTERNAL_PLATFORM_ID}-tick`,
|
|
56
|
+
platformId: RUNTIME_INTERNAL_PLATFORM_ID,
|
|
57
|
+
capability: RUNTIME_INTERNAL_CAPABILITY,
|
|
58
|
+
channel: RUNTIME_INTERNAL_CHANNEL,
|
|
59
|
+
startedAt: timestamp,
|
|
60
|
+
});
|
|
61
|
+
const status = isFailureCycle(cycle.status) ? "failed" : "succeeded";
|
|
62
|
+
const failureClass = status === "failed" ? cycleStatusFailureClass(cycle.status) : undefined;
|
|
63
|
+
await telemetry.completeAttempt(traceId, status, undefined, failureClass);
|
|
64
|
+
return { traceId, decisionId, attemptId };
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function mapCycleStatus(status) {
|
|
69
|
+
switch (status) {
|
|
70
|
+
case "intent_selected":
|
|
71
|
+
return "intent_selected";
|
|
72
|
+
case "denied":
|
|
73
|
+
return "denied";
|
|
74
|
+
case "deferred":
|
|
75
|
+
return "deferred";
|
|
76
|
+
case "delivery_unavailable":
|
|
77
|
+
return "delivery_unavailable";
|
|
78
|
+
case "runtime_carrier_only":
|
|
79
|
+
return "runtime_carrier_only";
|
|
80
|
+
case "heartbeat_ok":
|
|
81
|
+
default:
|
|
82
|
+
return "heartbeat_ok";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isFailureCycle(status) {
|
|
86
|
+
return status === "delivery_unavailable" || status === "denied";
|
|
87
|
+
}
|
|
88
|
+
function cycleStatusFailureClass(status) {
|
|
89
|
+
if (status === "delivery_unavailable")
|
|
90
|
+
return "delivery_unavailable";
|
|
91
|
+
if (status === "denied")
|
|
92
|
+
return "decision_denied";
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
@@ -6,99 +6,99 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import * as schema from "./schema/index.js";
|
|
7
7
|
// Pre-initialize sql.js WASM at module load time
|
|
8
8
|
const SQL = await initSqlJs();
|
|
9
|
-
const STATE_SCHEMA_SQL = `
|
|
10
|
-
CREATE TABLE IF NOT EXISTS credential_records (
|
|
11
|
-
platform_id TEXT PRIMARY KEY,
|
|
12
|
-
credential_type TEXT NOT NULL,
|
|
13
|
-
encrypted_value TEXT NOT NULL,
|
|
14
|
-
status TEXT NOT NULL,
|
|
15
|
-
verification_code TEXT,
|
|
16
|
-
challenge_text TEXT,
|
|
17
|
-
expires_at TEXT,
|
|
18
|
-
attempts_remaining INTEGER,
|
|
19
|
-
updated_at TEXT NOT NULL
|
|
20
|
-
);
|
|
21
|
-
CREATE TABLE IF NOT EXISTS policy_records (
|
|
22
|
-
platform_id TEXT PRIMARY KEY,
|
|
23
|
-
social_daily_limit INTEGER NOT NULL,
|
|
24
|
-
quiet_enabled INTEGER NOT NULL,
|
|
25
|
-
outreach_daily_budget INTEGER NOT NULL DEFAULT 2,
|
|
26
|
-
updated_at TEXT NOT NULL
|
|
27
|
-
);
|
|
28
|
-
CREATE TABLE IF NOT EXISTS life_evidence_index (
|
|
29
|
-
id TEXT PRIMARY KEY,
|
|
30
|
-
timestamp TEXT NOT NULL,
|
|
31
|
-
evidence_type TEXT NOT NULL,
|
|
32
|
-
sensitivity TEXT NOT NULL,
|
|
33
|
-
producer TEXT NOT NULL,
|
|
34
|
-
artifact_path TEXT NOT NULL,
|
|
35
|
-
platform_id TEXT,
|
|
36
|
-
source_refs_json TEXT NOT NULL
|
|
37
|
-
);
|
|
38
|
-
CREATE TABLE IF NOT EXISTS asset_registry (
|
|
39
|
-
id TEXT PRIMARY KEY,
|
|
40
|
-
kind TEXT NOT NULL,
|
|
41
|
-
path TEXT NOT NULL,
|
|
42
|
-
hash TEXT NOT NULL,
|
|
43
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
44
|
-
layer TEXT NOT NULL,
|
|
45
|
-
last_indexed_at TEXT NOT NULL
|
|
46
|
-
);
|
|
47
|
-
CREATE UNIQUE INDEX IF NOT EXISTS asset_registry_path_idx ON asset_registry(path);
|
|
48
|
-
CREATE TABLE IF NOT EXISTS intent_commit_records (
|
|
49
|
-
id TEXT PRIMARY KEY,
|
|
50
|
-
intent_id TEXT NOT NULL,
|
|
51
|
-
decision_id TEXT NOT NULL,
|
|
52
|
-
checkpoint_id TEXT,
|
|
53
|
-
state TEXT NOT NULL,
|
|
54
|
-
outcome_ref TEXT,
|
|
55
|
-
metadata_json TEXT,
|
|
56
|
-
updated_at TEXT NOT NULL
|
|
57
|
-
);
|
|
58
|
-
CREATE TABLE IF NOT EXISTS proposal_records (
|
|
59
|
-
id TEXT PRIMARY KEY,
|
|
60
|
-
target_asset_id TEXT NOT NULL,
|
|
61
|
-
before_hash TEXT,
|
|
62
|
-
after_hash TEXT,
|
|
63
|
-
status TEXT NOT NULL,
|
|
64
|
-
proposed_diff TEXT NOT NULL,
|
|
65
|
-
reason TEXT NOT NULL,
|
|
66
|
-
supporting_sources TEXT NOT NULL,
|
|
67
|
-
confidence REAL NOT NULL,
|
|
68
|
-
created_at TEXT NOT NULL,
|
|
69
|
-
applied_at TEXT
|
|
70
|
-
);
|
|
71
|
-
CREATE TABLE IF NOT EXISTS provenance_edges (
|
|
72
|
-
id TEXT PRIMARY KEY,
|
|
73
|
-
from_id TEXT NOT NULL,
|
|
74
|
-
to_id TEXT NOT NULL,
|
|
75
|
-
kind TEXT NOT NULL,
|
|
76
|
-
created_at TEXT NOT NULL
|
|
77
|
-
);
|
|
78
|
-
CREATE TABLE IF NOT EXISTS delivery_attempts (
|
|
79
|
-
attempt_id TEXT PRIMARY KEY,
|
|
80
|
-
decision_id TEXT NOT NULL,
|
|
81
|
-
target TEXT,
|
|
82
|
-
channel TEXT,
|
|
83
|
-
status TEXT NOT NULL,
|
|
84
|
-
message_id TEXT,
|
|
85
|
-
host_proof_ref_json TEXT,
|
|
86
|
-
error_class TEXT,
|
|
87
|
-
fallback_ref TEXT,
|
|
88
|
-
created_at TEXT NOT NULL
|
|
89
|
-
);
|
|
90
|
-
CREATE INDEX IF NOT EXISTS delivery_attempt_decision_idx ON delivery_attempts(decision_id);
|
|
91
|
-
CREATE TABLE IF NOT EXISTS operator_fallback_artifacts (
|
|
92
|
-
fallback_ref TEXT PRIMARY KEY,
|
|
93
|
-
decision_id TEXT NOT NULL,
|
|
94
|
-
status TEXT NOT NULL,
|
|
95
|
-
reason TEXT NOT NULL,
|
|
96
|
-
source_refs_json TEXT NOT NULL,
|
|
97
|
-
candidate_message TEXT,
|
|
98
|
-
next_step TEXT NOT NULL,
|
|
99
|
-
created_at TEXT NOT NULL
|
|
100
|
-
);
|
|
101
|
-
CREATE INDEX IF NOT EXISTS operator_fallback_decision_idx ON operator_fallback_artifacts(decision_id);
|
|
9
|
+
const STATE_SCHEMA_SQL = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS credential_records (
|
|
11
|
+
platform_id TEXT PRIMARY KEY,
|
|
12
|
+
credential_type TEXT NOT NULL,
|
|
13
|
+
encrypted_value TEXT NOT NULL,
|
|
14
|
+
status TEXT NOT NULL,
|
|
15
|
+
verification_code TEXT,
|
|
16
|
+
challenge_text TEXT,
|
|
17
|
+
expires_at TEXT,
|
|
18
|
+
attempts_remaining INTEGER,
|
|
19
|
+
updated_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS policy_records (
|
|
22
|
+
platform_id TEXT PRIMARY KEY,
|
|
23
|
+
social_daily_limit INTEGER NOT NULL,
|
|
24
|
+
quiet_enabled INTEGER NOT NULL,
|
|
25
|
+
outreach_daily_budget INTEGER NOT NULL DEFAULT 2,
|
|
26
|
+
updated_at TEXT NOT NULL
|
|
27
|
+
);
|
|
28
|
+
CREATE TABLE IF NOT EXISTS life_evidence_index (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
timestamp TEXT NOT NULL,
|
|
31
|
+
evidence_type TEXT NOT NULL,
|
|
32
|
+
sensitivity TEXT NOT NULL,
|
|
33
|
+
producer TEXT NOT NULL,
|
|
34
|
+
artifact_path TEXT NOT NULL,
|
|
35
|
+
platform_id TEXT,
|
|
36
|
+
source_refs_json TEXT NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS asset_registry (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
path TEXT NOT NULL,
|
|
42
|
+
hash TEXT NOT NULL,
|
|
43
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
44
|
+
layer TEXT NOT NULL,
|
|
45
|
+
last_indexed_at TEXT NOT NULL
|
|
46
|
+
);
|
|
47
|
+
CREATE UNIQUE INDEX IF NOT EXISTS asset_registry_path_idx ON asset_registry(path);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS intent_commit_records (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
intent_id TEXT NOT NULL,
|
|
51
|
+
decision_id TEXT NOT NULL,
|
|
52
|
+
checkpoint_id TEXT,
|
|
53
|
+
state TEXT NOT NULL,
|
|
54
|
+
outcome_ref TEXT,
|
|
55
|
+
metadata_json TEXT,
|
|
56
|
+
updated_at TEXT NOT NULL
|
|
57
|
+
);
|
|
58
|
+
CREATE TABLE IF NOT EXISTS proposal_records (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
target_asset_id TEXT NOT NULL,
|
|
61
|
+
before_hash TEXT,
|
|
62
|
+
after_hash TEXT,
|
|
63
|
+
status TEXT NOT NULL,
|
|
64
|
+
proposed_diff TEXT NOT NULL,
|
|
65
|
+
reason TEXT NOT NULL,
|
|
66
|
+
supporting_sources TEXT NOT NULL,
|
|
67
|
+
confidence REAL NOT NULL,
|
|
68
|
+
created_at TEXT NOT NULL,
|
|
69
|
+
applied_at TEXT
|
|
70
|
+
);
|
|
71
|
+
CREATE TABLE IF NOT EXISTS provenance_edges (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
from_id TEXT NOT NULL,
|
|
74
|
+
to_id TEXT NOT NULL,
|
|
75
|
+
kind TEXT NOT NULL,
|
|
76
|
+
created_at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
CREATE TABLE IF NOT EXISTS delivery_attempts (
|
|
79
|
+
attempt_id TEXT PRIMARY KEY,
|
|
80
|
+
decision_id TEXT NOT NULL,
|
|
81
|
+
target TEXT,
|
|
82
|
+
channel TEXT,
|
|
83
|
+
status TEXT NOT NULL,
|
|
84
|
+
message_id TEXT,
|
|
85
|
+
host_proof_ref_json TEXT,
|
|
86
|
+
error_class TEXT,
|
|
87
|
+
fallback_ref TEXT,
|
|
88
|
+
created_at TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS delivery_attempt_decision_idx ON delivery_attempts(decision_id);
|
|
91
|
+
CREATE TABLE IF NOT EXISTS operator_fallback_artifacts (
|
|
92
|
+
fallback_ref TEXT PRIMARY KEY,
|
|
93
|
+
decision_id TEXT NOT NULL,
|
|
94
|
+
status TEXT NOT NULL,
|
|
95
|
+
reason TEXT NOT NULL,
|
|
96
|
+
source_refs_json TEXT NOT NULL,
|
|
97
|
+
candidate_message TEXT,
|
|
98
|
+
next_step TEXT NOT NULL,
|
|
99
|
+
created_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS operator_fallback_decision_idx ON operator_fallback_artifacts(decision_id);
|
|
102
102
|
`;
|
|
103
103
|
function resolveDbPath(filename) {
|
|
104
104
|
if (path.isAbsolute(filename) || filename === ":memory:") {
|
package/workspace-ops-bridge.js
CHANGED
|
@@ -39,6 +39,7 @@ export async function openWorkspaceOpsBridge(workspaceRoot) {
|
|
|
39
39
|
const opsRouter = cliIndex.createOpsRouter({
|
|
40
40
|
runtimeAvailable: runtimeResolved.ok,
|
|
41
41
|
readModels: deps.readModels,
|
|
42
|
+
runtimeRecorder: deps.runtimeRecorder,
|
|
42
43
|
});
|
|
43
44
|
const commands = commandsMod.createCliCommands({
|
|
44
45
|
readModels: deps.readModels,
|