memorix 1.4.0 → 1.4.2
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +9 -9
- package/README.md +7 -2
- package/README.zh-CN.md +6 -2
- package/dist/cli/index.js +1216 -485
- package/dist/index.js +984 -337
- package/dist/maintenance-runner.js +564 -180
- package/dist/media-video-runner.js +564 -180
- package/dist/memcode/index.js +53 -21
- package/dist/memcode-runtime/CHANGELOG.md +31 -0
- package/dist/sdk.js +983 -336
- package/dist/vector-backfill-runner.js +564 -180
- package/docs/1.4.2-EVIDENCE-GOVERNED-MEMORY-SPEC.md +434 -0
- package/docs/API_REFERENCE.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +52 -128
- package/package.json +5 -2
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/codegraph.ts +83 -2
- package/src/codegraph/auto-context.ts +20 -0
- package/src/codegraph/current-facts.ts +1 -0
- package/src/codegraph/external-provider.ts +1 -0
- package/src/codegraph/store.ts +216 -5
- package/src/codegraph/types.ts +42 -0
- package/src/knowledge/context-assembly.ts +25 -0
- package/src/knowledge/governor.ts +156 -0
- package/src/knowledge/outcome-store.ts +102 -0
- package/src/knowledge/outcome-types.ts +49 -0
- package/src/knowledge/workflows.ts +32 -1
- package/src/knowledge/workset.ts +203 -13
- package/src/store/sqlite-db.ts +42 -0
- package/docs/dev-log/progress.txt +0 -388
|
@@ -43,6 +43,22 @@ export interface ContextReceiptOmission {
|
|
|
43
43
|
count: number;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export interface ContextReceiptGovernanceDecision {
|
|
47
|
+
kind: ContextCandidateKind;
|
|
48
|
+
id?: string;
|
|
49
|
+
disposition: 'include' | 'compact' | 'defer' | 'exclude';
|
|
50
|
+
reasons: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Privacy-safe evidence qualification, never appended to the agent prompt. */
|
|
54
|
+
export interface ContextReceiptGovernance {
|
|
55
|
+
/** Current implementation qualifies optional memory/knowledge, not caller task facts. */
|
|
56
|
+
scope: 'optional-evidence';
|
|
57
|
+
mode: 'abstain' | 'card' | 'workset';
|
|
58
|
+
decisions: ContextReceiptGovernanceDecision[];
|
|
59
|
+
cautions: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
export interface ContextReceipt {
|
|
47
63
|
version: '1.3';
|
|
48
64
|
target: ContextDeliveryTarget;
|
|
@@ -53,6 +69,7 @@ export interface ContextReceipt {
|
|
|
53
69
|
};
|
|
54
70
|
selected: ContextReceiptSelection[];
|
|
55
71
|
omitted: ContextReceiptOmission[];
|
|
72
|
+
governance?: ContextReceiptGovernance;
|
|
56
73
|
scheduledActions: string[];
|
|
57
74
|
}
|
|
58
75
|
|
|
@@ -92,6 +109,14 @@ export function formatContextReceipt(receipt: ContextReceipt): string {
|
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
|
|
112
|
+
if (receipt.governance?.decisions.length) {
|
|
113
|
+
lines.push('', 'Optional evidence qualification');
|
|
114
|
+
for (const item of receipt.governance.decisions.slice(0, 20)) {
|
|
115
|
+
const id = item.id ? ` ${item.id}` : '';
|
|
116
|
+
lines.push(`- ${item.kind}${id}: ${item.disposition} (${item.reasons.join(', ')})`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
95
120
|
if (receipt.scheduledActions.length > 0) {
|
|
96
121
|
lines.push('', 'Scheduled follow-up');
|
|
97
122
|
for (const action of receipt.scheduledActions) lines.push(`- ${action}`);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContextCandidateFreshness,
|
|
3
|
+
ContextCandidateKind,
|
|
4
|
+
ContextCandidateTrust,
|
|
5
|
+
} from './context-assembly.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A small, serializable description of an item competing for agent context.
|
|
9
|
+
* This is intentionally independent from stores and prompt rendering so every
|
|
10
|
+
* delivery surface can apply the same evidence rules.
|
|
11
|
+
*/
|
|
12
|
+
export interface GovernanceCandidate {
|
|
13
|
+
kind: ContextCandidateKind;
|
|
14
|
+
id?: string;
|
|
15
|
+
estimatedTokens: number;
|
|
16
|
+
relevance: number;
|
|
17
|
+
scopeAllowed: boolean;
|
|
18
|
+
freshness?: ContextCandidateFreshness;
|
|
19
|
+
trust?: ContextCandidateTrust;
|
|
20
|
+
quality?: 'verified' | 'probationary' | 'degraded' | 'blocked';
|
|
21
|
+
conflict?: 'none' | 'possible' | 'confirmed';
|
|
22
|
+
evidenceCount?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type GovernanceDisposition = 'include' | 'compact' | 'defer' | 'exclude';
|
|
26
|
+
|
|
27
|
+
export type GovernanceReason =
|
|
28
|
+
| 'scope-forbidden'
|
|
29
|
+
| 'confirmed-conflict'
|
|
30
|
+
| 'blocked-quality'
|
|
31
|
+
| 'degraded-quality'
|
|
32
|
+
| 'stale-evidence'
|
|
33
|
+
| 'unknown-freshness'
|
|
34
|
+
| 'possible-conflict'
|
|
35
|
+
| 'probationary-evidence'
|
|
36
|
+
| 'token-budget'
|
|
37
|
+
| 'current-source-backed-evidence'
|
|
38
|
+
| 'relevant-evidence'
|
|
39
|
+
| 'no-qualified-evidence';
|
|
40
|
+
|
|
41
|
+
export interface GovernanceDecision {
|
|
42
|
+
candidate: GovernanceCandidate;
|
|
43
|
+
disposition: GovernanceDisposition;
|
|
44
|
+
reasons: GovernanceReason[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GovernancePlan {
|
|
48
|
+
mode: 'abstain' | 'card' | 'workset';
|
|
49
|
+
budget: { maxTokens: number; usedTokens: number };
|
|
50
|
+
decisions: GovernanceDecision[];
|
|
51
|
+
cautions: GovernanceReason[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clampRelevance(value: number): number {
|
|
55
|
+
if (!Number.isFinite(value)) return 0;
|
|
56
|
+
return Math.max(0, Math.min(1, value));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function classify(candidate: GovernanceCandidate): Omit<GovernanceDecision, 'candidate'> {
|
|
60
|
+
const freshness = candidate.freshness ?? 'unknown';
|
|
61
|
+
const trust = candidate.trust ?? 'historical';
|
|
62
|
+
const quality = candidate.quality ?? 'probationary';
|
|
63
|
+
const conflict = candidate.conflict ?? 'none';
|
|
64
|
+
|
|
65
|
+
if (!candidate.scopeAllowed) {
|
|
66
|
+
return { disposition: 'exclude', reasons: ['scope-forbidden'] };
|
|
67
|
+
}
|
|
68
|
+
if (conflict === 'confirmed') {
|
|
69
|
+
return { disposition: 'exclude', reasons: ['confirmed-conflict'] };
|
|
70
|
+
}
|
|
71
|
+
if (quality === 'blocked') {
|
|
72
|
+
return { disposition: 'exclude', reasons: ['blocked-quality'] };
|
|
73
|
+
}
|
|
74
|
+
if (quality === 'degraded') {
|
|
75
|
+
return { disposition: 'defer', reasons: ['degraded-quality'] };
|
|
76
|
+
}
|
|
77
|
+
if (freshness === 'stale') {
|
|
78
|
+
return { disposition: 'defer', reasons: ['stale-evidence'] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const reasons: GovernanceReason[] = [];
|
|
82
|
+
let disposition: GovernanceDisposition = 'include';
|
|
83
|
+
if (freshness === 'unknown') {
|
|
84
|
+
disposition = 'compact';
|
|
85
|
+
reasons.push('unknown-freshness');
|
|
86
|
+
}
|
|
87
|
+
if (conflict === 'possible') {
|
|
88
|
+
disposition = 'compact';
|
|
89
|
+
reasons.push('possible-conflict');
|
|
90
|
+
}
|
|
91
|
+
if (quality === 'probationary') {
|
|
92
|
+
disposition = 'compact';
|
|
93
|
+
reasons.push('probationary-evidence');
|
|
94
|
+
}
|
|
95
|
+
if (disposition === 'include' && freshness === 'current' && trust === 'source-backed') {
|
|
96
|
+
reasons.push('current-source-backed-evidence');
|
|
97
|
+
} else if (reasons.length === 0) {
|
|
98
|
+
reasons.push('relevant-evidence');
|
|
99
|
+
}
|
|
100
|
+
return { disposition, reasons };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function priority(decision: GovernanceDecision): number {
|
|
104
|
+
const dispositionWeight: Record<GovernanceDisposition, number> = {
|
|
105
|
+
include: 3,
|
|
106
|
+
compact: 2,
|
|
107
|
+
defer: 1,
|
|
108
|
+
exclude: 0,
|
|
109
|
+
};
|
|
110
|
+
const evidence = Math.min(3, decision.candidate.evidenceCount ?? 0) / 10;
|
|
111
|
+
return dispositionWeight[decision.disposition] + clampRelevance(decision.candidate.relevance) + evidence;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Decide which evidence may enter a bounded context response. The result never
|
|
116
|
+
* mutates memory and deliberately prefers an explicit abstention to a generic
|
|
117
|
+
* history dump.
|
|
118
|
+
*/
|
|
119
|
+
export function governContextCandidates(
|
|
120
|
+
candidates: GovernanceCandidate[],
|
|
121
|
+
maxTokens: number,
|
|
122
|
+
): GovernancePlan {
|
|
123
|
+
const safeBudget = Math.max(0, Math.floor(maxTokens));
|
|
124
|
+
const decisions = candidates.map(candidate => ({ candidate, ...classify(candidate) }));
|
|
125
|
+
const eligible = decisions
|
|
126
|
+
.filter(decision => decision.disposition === 'include' || decision.disposition === 'compact')
|
|
127
|
+
.sort((left, right) => priority(right) - priority(left));
|
|
128
|
+
let usedTokens = 0;
|
|
129
|
+
|
|
130
|
+
for (const decision of eligible) {
|
|
131
|
+
const tokens = Math.max(0, Math.ceil(decision.candidate.estimatedTokens));
|
|
132
|
+
if (usedTokens + tokens <= safeBudget) {
|
|
133
|
+
usedTokens += tokens;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
decision.disposition = 'defer';
|
|
137
|
+
decision.reasons = [...decision.reasons, 'token-budget'];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const delivered = decisions.filter(decision => decision.disposition === 'include' || decision.disposition === 'compact');
|
|
141
|
+
const mode = delivered.length === 0
|
|
142
|
+
? 'abstain'
|
|
143
|
+
: delivered.some(decision => decision.disposition === 'include')
|
|
144
|
+
? 'workset'
|
|
145
|
+
: 'card';
|
|
146
|
+
const cautions = [...new Set(decisions
|
|
147
|
+
.filter(decision => decision.disposition !== 'include')
|
|
148
|
+
.flatMap(decision => decision.reasons))];
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
mode,
|
|
152
|
+
budget: { maxTokens: safeBudget, usedTokens },
|
|
153
|
+
decisions,
|
|
154
|
+
cautions: cautions.length > 0 ? cautions : ['no-qualified-evidence'],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
3
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
4
|
+
import {
|
|
5
|
+
MEMORY_OUTCOME_CANDIDATE_KINDS,
|
|
6
|
+
MEMORY_OUTCOME_SIGNAL_KINDS,
|
|
7
|
+
type MemoryOutcomeCandidateKind,
|
|
8
|
+
type MemoryOutcomeSignal,
|
|
9
|
+
type MemoryOutcomeSignalKind,
|
|
10
|
+
} from './outcome-types.js';
|
|
11
|
+
|
|
12
|
+
function optionalText(value: unknown): string | undefined {
|
|
13
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function text(value: string, field: string, max = 1_000): string {
|
|
17
|
+
const safe = sanitizeCredentials(value).replace(/\s+/g, ' ').trim();
|
|
18
|
+
if (!safe) throw new Error('Outcome signal ' + field + ' is required.');
|
|
19
|
+
if (safe.length > max) throw new Error('Outcome signal ' + field + ' is too long.');
|
|
20
|
+
return safe;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function oneOf<T extends readonly string[]>(value: unknown, values: T, field: string): T[number] {
|
|
24
|
+
if (typeof value !== 'string' || !(values as readonly string[]).includes(value)) {
|
|
25
|
+
throw new Error('Outcome signal ' + field + ' is invalid.');
|
|
26
|
+
}
|
|
27
|
+
return value as T[number];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function rowToSignal(row: any): MemoryOutcomeSignal {
|
|
31
|
+
return {
|
|
32
|
+
id: row.id,
|
|
33
|
+
projectId: row.projectId,
|
|
34
|
+
candidateKind: row.candidateKind,
|
|
35
|
+
candidateId: row.candidateId,
|
|
36
|
+
kind: row.kind,
|
|
37
|
+
sourceRef: row.sourceRef,
|
|
38
|
+
...(optionalText(row.snapshotId) ? { snapshotId: row.snapshotId } : {}),
|
|
39
|
+
...(optionalText(row.detail) ? { detail: row.detail } : {}),
|
|
40
|
+
observedAt: row.observedAt,
|
|
41
|
+
} as MemoryOutcomeSignal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Persistence only; callers decide which real-world event is worth recording. */
|
|
45
|
+
export class OutcomeStore {
|
|
46
|
+
private db: any = null;
|
|
47
|
+
|
|
48
|
+
async init(dataDir: string): Promise<void> {
|
|
49
|
+
this.db = getDatabase(dataDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
record(input: Omit<MemoryOutcomeSignal, 'id' | 'observedAt'> & { observedAt?: string }): MemoryOutcomeSignal {
|
|
53
|
+
const signal: MemoryOutcomeSignal = {
|
|
54
|
+
id: randomUUID(),
|
|
55
|
+
projectId: text(input.projectId, 'project id'),
|
|
56
|
+
candidateKind: oneOf(input.candidateKind, MEMORY_OUTCOME_CANDIDATE_KINDS, 'candidate kind'),
|
|
57
|
+
candidateId: text(input.candidateId, 'candidate id'),
|
|
58
|
+
kind: oneOf(input.kind, MEMORY_OUTCOME_SIGNAL_KINDS, 'kind'),
|
|
59
|
+
sourceRef: text(input.sourceRef, 'source reference'),
|
|
60
|
+
...(input.snapshotId ? { snapshotId: text(input.snapshotId, 'snapshot id') } : {}),
|
|
61
|
+
...(input.detail ? { detail: text(input.detail, 'detail') } : {}),
|
|
62
|
+
observedAt: input.observedAt ?? new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
this.db.prepare(`
|
|
65
|
+
INSERT INTO memory_outcome_signals (
|
|
66
|
+
id, projectId, candidateKind, candidateId, kind, sourceRef, snapshotId, detail, observedAt
|
|
67
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
68
|
+
`).run(
|
|
69
|
+
signal.id,
|
|
70
|
+
signal.projectId,
|
|
71
|
+
signal.candidateKind,
|
|
72
|
+
signal.candidateId,
|
|
73
|
+
signal.kind,
|
|
74
|
+
signal.sourceRef,
|
|
75
|
+
signal.snapshotId ?? null,
|
|
76
|
+
signal.detail ?? null,
|
|
77
|
+
signal.observedAt,
|
|
78
|
+
);
|
|
79
|
+
return signal;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
latestForCandidates(
|
|
83
|
+
projectId: string,
|
|
84
|
+
candidateKind: MemoryOutcomeCandidateKind,
|
|
85
|
+
candidateIds: string[],
|
|
86
|
+
): Map<string, MemoryOutcomeSignal> {
|
|
87
|
+
const ids = [...new Set(candidateIds.filter(Boolean))];
|
|
88
|
+
if (ids.length === 0) return new Map();
|
|
89
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
90
|
+
const rows = this.db.prepare(`
|
|
91
|
+
SELECT * FROM memory_outcome_signals
|
|
92
|
+
WHERE projectId = ? AND candidateKind = ? AND candidateId IN (${placeholders})
|
|
93
|
+
ORDER BY observedAt DESC, id DESC
|
|
94
|
+
`).all(projectId, candidateKind, ...ids);
|
|
95
|
+
const latest = new Map<string, MemoryOutcomeSignal>();
|
|
96
|
+
for (const row of rows) {
|
|
97
|
+
const signal = rowToSignal(row);
|
|
98
|
+
if (!latest.has(signal.candidateId)) latest.set(signal.candidateId, signal);
|
|
99
|
+
}
|
|
100
|
+
return latest;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only evidence about whether a context artifact helped or was
|
|
3
|
+
* contradicted. Signals never replace the artifact they describe.
|
|
4
|
+
*/
|
|
5
|
+
export const MEMORY_OUTCOME_CANDIDATE_KINDS = ['claim', 'durable-memory', 'workflow'] as const;
|
|
6
|
+
export type MemoryOutcomeCandidateKind = typeof MEMORY_OUTCOME_CANDIDATE_KINDS[number];
|
|
7
|
+
|
|
8
|
+
export const MEMORY_OUTCOME_SIGNAL_KINDS = [
|
|
9
|
+
'verification-passed',
|
|
10
|
+
'verification-failed',
|
|
11
|
+
'verified-reuse',
|
|
12
|
+
'user-pin',
|
|
13
|
+
'user-correction',
|
|
14
|
+
'source-changed',
|
|
15
|
+
'conflict-confirmed',
|
|
16
|
+
'manual-review',
|
|
17
|
+
] as const;
|
|
18
|
+
export type MemoryOutcomeSignalKind = typeof MEMORY_OUTCOME_SIGNAL_KINDS[number];
|
|
19
|
+
|
|
20
|
+
export interface MemoryOutcomeSignal {
|
|
21
|
+
id: string;
|
|
22
|
+
projectId: string;
|
|
23
|
+
candidateKind: MemoryOutcomeCandidateKind;
|
|
24
|
+
candidateId: string;
|
|
25
|
+
kind: MemoryOutcomeSignalKind;
|
|
26
|
+
sourceRef: string;
|
|
27
|
+
snapshotId?: string;
|
|
28
|
+
detail?: string;
|
|
29
|
+
observedAt: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type OutcomeQuality = 'verified' | 'probationary' | 'degraded';
|
|
33
|
+
|
|
34
|
+
/** The newest auditable signal wins; missing signals leave lifecycle state in charge. */
|
|
35
|
+
export function qualityFromOutcome(signal?: Pick<MemoryOutcomeSignal, 'kind'>): OutcomeQuality | undefined {
|
|
36
|
+
if (!signal) return undefined;
|
|
37
|
+
if (signal.kind === 'verification-failed'
|
|
38
|
+
|| signal.kind === 'user-correction'
|
|
39
|
+
|| signal.kind === 'source-changed'
|
|
40
|
+
|| signal.kind === 'conflict-confirmed') {
|
|
41
|
+
return 'degraded';
|
|
42
|
+
}
|
|
43
|
+
if (signal.kind === 'verification-passed'
|
|
44
|
+
|| signal.kind === 'verified-reuse'
|
|
45
|
+
|| signal.kind === 'user-pin') {
|
|
46
|
+
return 'verified';
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -7,6 +7,8 @@ import { atomicWriteFile, withFileLock } from '../store/file-lock.js';
|
|
|
7
7
|
import { WorkflowSyncer } from '../workspace/workflow-sync.js';
|
|
8
8
|
import { getKnowledgeWorkspacePaths, resolveKnowledgeWorkspaceFile } from './workspace.js';
|
|
9
9
|
import { WorkflowStore } from './workflow-store.js';
|
|
10
|
+
import { OutcomeStore } from './outcome-store.js';
|
|
11
|
+
import type { MemoryOutcomeCandidateKind, MemoryOutcomeSignalKind } from './outcome-types.js';
|
|
10
12
|
import { containsTaskKeyword } from '../codegraph/task-lens.js';
|
|
11
13
|
import type {
|
|
12
14
|
WorkflowAdapterPreview,
|
|
@@ -751,7 +753,36 @@ export async function recordWorkflowRun(input: {
|
|
|
751
753
|
throw new Error('Workflow was not found for this workspace');
|
|
752
754
|
}
|
|
753
755
|
if (!input.run.task.trim()) throw new Error('Workflow run task is required');
|
|
754
|
-
|
|
756
|
+
const run = store.recordRun(input.run);
|
|
757
|
+
const kind: MemoryOutcomeSignalKind | undefined = run.outcome === 'passed' && run.verificationVerdict === 'passed'
|
|
758
|
+
? 'verification-passed'
|
|
759
|
+
: run.outcome === 'failed' || run.verificationVerdict === 'failed'
|
|
760
|
+
? 'verification-failed'
|
|
761
|
+
: undefined;
|
|
762
|
+
if (!kind) return run;
|
|
763
|
+
|
|
764
|
+
const evidence = new Map<string, MemoryOutcomeCandidateKind>();
|
|
765
|
+
evidence.set(workflow.id, 'workflow');
|
|
766
|
+
for (const reference of run.selectedEvidence) {
|
|
767
|
+
const match = /^(claim|durable):(.+)$/.exec(reference);
|
|
768
|
+
if (!match) continue;
|
|
769
|
+
evidence.set(match[2], match[1] === 'claim' ? 'claim' : 'durable-memory');
|
|
770
|
+
}
|
|
771
|
+
const outcomes = new OutcomeStore();
|
|
772
|
+
await outcomes.init(workflowStoreDataDir(input.workspace));
|
|
773
|
+
for (const [candidateId, candidateKind] of evidence) {
|
|
774
|
+
outcomes.record({
|
|
775
|
+
projectId: run.projectId,
|
|
776
|
+
candidateKind,
|
|
777
|
+
candidateId,
|
|
778
|
+
kind,
|
|
779
|
+
sourceRef: 'workflow-run:' + run.id,
|
|
780
|
+
...(run.startingSnapshotId ? { snapshotId: run.startingSnapshotId } : {}),
|
|
781
|
+
...(run.failureReason ? { detail: run.failureReason } : {}),
|
|
782
|
+
observedAt: run.completedAt ?? run.startedAt,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
return run;
|
|
755
786
|
}
|
|
756
787
|
|
|
757
788
|
export async function selectWorkspaceWorkflows(input: {
|