minimal-agent 0.18.13 → 0.18.14
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/package.json +1 -3
- package/plugins/workflow-runner/src/runtime/context.js +1 -1
- package/plugins/workflow-runner/src/runtime/context.ts +1 -1
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
- package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
- package/skills/ssh-remote-dev/SKILL.md +61 -0
- package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
- package/src/advisor/README.md +62 -24
- package/src/advisor/advisorNudge.js +199 -0
- package/src/advisor/advisorNudge.ts +232 -0
- package/src/advisor/deliveryReview.js +35 -12
- package/src/advisor/deliveryReview.ts +35 -11
- package/src/advisor/presentation.js +7 -10
- package/src/advisor/presentation.ts +10 -13
- package/src/advisor/reviewContext.js +22 -58
- package/src/advisor/reviewContext.ts +24 -67
- package/src/cli/configMenu.js +17 -128
- package/src/cli/configMenu.tsx +17 -193
- package/src/cli/print.js +2 -0
- package/src/cli/print.ts +2 -0
- package/src/cli/streamJson.js +2 -0
- package/src/cli/streamJson.ts +2 -0
- package/src/cli/wizards/advisorWizard.js +9 -7
- package/src/cli/wizards/advisorWizard.tsx +9 -7
- package/src/cli/wizards/contextAdvisorWizard.js +9 -7
- package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
- package/src/cli/wizards/providerPresets.js +27 -1
- package/src/cli/wizards/providerPresets.ts +44 -2
- package/src/cli/wizards/providerWizard.js +51 -10
- package/src/cli/wizards/providerWizard.tsx +59 -9
- package/src/config/configDefaults.js +1 -1
- package/src/config/configDefaults.ts +1 -1
- package/src/config/configDiagnostics.js +1 -21
- package/src/config/configDiagnostics.ts +3 -23
- package/src/config/configFile.js +11 -4
- package/src/config/configFile.ts +27 -7
- package/src/config/configManagement.js +1 -0
- package/src/config/configManagement.ts +2 -0
- package/src/config/configService.js +12 -8
- package/src/config/configService.ts +16 -11
- package/src/config/numericLimits.js +0 -4
- package/src/config/numericLimits.ts +0 -4
- package/src/config/reasoningEffort.js +16 -0
- package/src/config/reasoningEffort.ts +18 -0
- package/src/context/budgetPlan.js +1 -1
- package/src/context/budgetPlan.ts +1 -1
- package/src/context/optimizeConversation.js +1 -1
- package/src/context/optimizeConversation.ts +1 -1
- package/src/harness/stallGuard.js +8 -13
- package/src/harness/stallGuard.ts +8 -12
- package/src/llm/client.js +64 -17
- package/src/llm/client.ts +62 -7
- package/src/llm/providers/openaiFormat.js +14 -3
- package/src/llm/providers/openaiFormat.ts +17 -3
- package/src/llm/providers/responsesFormat.js +275 -0
- package/src/llm/providers/responsesFormat.ts +306 -0
- package/src/loop/README.md +18 -3
- package/src/loop/liveDiagramGate.js +63 -0
- package/src/loop/liveDiagramGate.ts +61 -0
- package/src/loop/messages.js +12 -0
- package/src/loop/messages.ts +13 -0
- package/src/loop/options.ts +7 -2
- package/src/loop/toolBatch.js +0 -1
- package/src/loop/toolBatch.ts +0 -1
- package/src/loop/toolExecutor.js +11 -46
- package/src/loop/toolExecutor.ts +12 -55
- package/src/loop/toolPresentation.js +2 -13
- package/src/loop/toolPresentation.ts +2 -13
- package/src/loop/toolSemantics.js +3 -4
- package/src/loop/toolSemantics.ts +4 -6
- package/src/loop.js +222 -214
- package/src/loop.ts +226 -232
- package/src/memory/dependencies.js +11 -3
- package/src/memory/dependencies.ts +11 -3
- package/src/memory/episodicMemory.js +59 -18
- package/src/memory/episodicMemory.ts +58 -18
- package/src/memory/experienceStore.js +4 -3
- package/src/memory/experienceStore.ts +4 -3
- package/src/memory/factCandidates.js +53 -1
- package/src/memory/factCandidates.ts +54 -1
- package/src/memory/factLearning.js +2 -2
- package/src/memory/factLearning.ts +2 -2
- package/src/memory/jobPolicy.js +24 -5
- package/src/memory/jobPolicy.ts +25 -5
- package/src/memory/learning.js +2 -2
- package/src/memory/learning.ts +2 -2
- package/src/memory/learningModel.js +7 -0
- package/src/memory/learningModel.ts +7 -0
- package/src/memory/managementService.js +8 -7
- package/src/memory/managementService.ts +16 -8
- package/src/memory/memoryService.js +104 -33
- package/src/memory/memoryService.ts +132 -28
- package/src/memory/projectKnowledge.js +39 -3
- package/src/memory/projectKnowledge.ts +45 -3
- package/src/memory/recallQuery.js +56 -0
- package/src/memory/recallQuery.ts +58 -0
- package/src/memory/retrieval.js +39 -10
- package/src/memory/retrieval.ts +46 -11
- package/src/memory/turnJournal.js +75 -10
- package/src/memory/turnJournal.ts +66 -10
- package/src/memory/types.ts +7 -1
- package/src/prompts/delegation.js +2 -1
- package/src/prompts/delegation.ts +3 -1
- package/src/prompts/policyRegistry.js +9 -12
- package/src/prompts/policyRegistry.ts +9 -12
- package/src/prompts/promptAssembly.js +6 -5
- package/src/prompts/promptAssembly.ts +8 -6
- package/src/prompts/visualPresentation.js +39 -10
- package/src/prompts/visualPresentation.ts +38 -9
- package/src/runtime/answerPreparation.js +1 -1
- package/src/runtime/answerPreparation.ts +1 -1
- package/src/runtime/fileLock.js +19 -3
- package/src/runtime/fileLock.ts +19 -3
- package/src/runtime/invocationPolicy.js +1 -1
- package/src/runtime/invocationPolicy.ts +1 -1
- package/src/runtime/outputContract.js +0 -4
- package/src/runtime/outputContract.ts +0 -7
- package/src/session/runAgentTurn.js +3 -0
- package/src/session/runAgentTurn.ts +3 -0
- package/src/team/onlineToolContract.js +0 -1
- package/src/team/onlineToolContract.ts +0 -1
- package/src/tools/advisor/advisor.js +207 -365
- package/src/tools/advisor/advisor.ts +211 -383
- package/src/tools/agent/agentTypes.js +0 -1
- package/src/tools/agent/agentTypes.ts +0 -1
- package/src/tools/agent/coordinator.js +4 -1
- package/src/tools/agent/coordinator.ts +4 -1
- package/src/tools/bash/semantics.js +4 -5
- package/src/tools/bash/semantics.ts +4 -5
- package/src/tools/capabilities.js +2 -3
- package/src/tools/capabilities.ts +2 -4
- package/src/tools/index.js +0 -16
- package/src/tools/index.ts +0 -16
- package/src/tools/memory/maintenance.js +8 -9
- package/src/tools/memory/maintenance.ts +13 -11
- package/src/tools/memory/memory.js +5 -1
- package/src/tools/memory/memory.ts +5 -0
- package/src/tools/optimizeConversation/README.md +4 -6
- package/src/tools/toolPromptGuides.js +2 -9
- package/src/tools/toolPromptGuides.ts +2 -9
- package/src/types.ts +28 -105
- package/src/ui/App.js +2 -0
- package/src/ui/App.tsx +1 -0
- package/src/ui/MessageList.js +8 -44
- package/src/ui/MessageList.tsx +3 -81
- package/src/ui/hooks/useChat.js +2 -0
- package/src/ui/hooks/useChat.ts +2 -0
- package/src/ui/toolProgress.js +1 -1
- package/src/ui/toolProgress.ts +1 -1
- package/src/web/protocol.ts +10 -3
- package/src/advisor/advisorCoordinator.js +0 -734
- package/src/advisor/advisorCoordinator.ts +0 -825
- package/src/cli/wizards/sshHostWizard.js +0 -156
- package/src/cli/wizards/sshHostWizard.tsx +0 -214
- package/src/context/sshHosts.js +0 -290
- package/src/context/sshHosts.ts +0 -312
- package/src/loop/publication.js +0 -94
- package/src/loop/publication.ts +0 -124
- package/src/runtime/finalAnswer.js +0 -9
- package/src/runtime/finalAnswer.ts +0 -13
- package/src/tools/advisor/advisorDecision.js +0 -79
- package/src/tools/advisor/advisorDecision.ts +0 -92
- package/src/tools/ssh/ssh.js +0 -152
- package/src/tools/ssh/ssh.ts +0 -167
- package/src/tools/ssh/sshAvailability.js +0 -35
- package/src/tools/ssh/sshAvailability.ts +0 -39
- package/src/tools/ssh/sshClient.js +0 -177
- package/src/tools/ssh/sshClient.ts +0 -189
- package/src/tools/ssh/sshDownload.js +0 -77
- package/src/tools/ssh/sshDownload.ts +0 -92
- package/src/tools/ssh/sshHostsTool.js +0 -36
- package/src/tools/ssh/sshHostsTool.ts +0 -48
- package/src/tools/ssh/sshUpload.js +0 -72
- package/src/tools/ssh/sshUpload.ts +0 -85
package/src/memory/learning.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
8
|
+
import { isPausedJob, jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -349,7 +349,7 @@ export function scheduleMemoryMaintenance(options) {
|
|
|
349
349
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled)
|
|
350
350
|
return;
|
|
351
351
|
const store = await readExperienceStore(options.workspaceId);
|
|
352
|
-
const pending = store.pending.filter(turn => !turn
|
|
352
|
+
const pending = store.pending.filter(turn => !isPausedJob(turn));
|
|
353
353
|
const continuing = pending.some(turn => turn.createdAt <= (store.runs.at(-1)?.startedAt ?? 0));
|
|
354
354
|
if (!pending.length || (!continuing && pending.length < settings.learning.minPendingTurns))
|
|
355
355
|
return;
|
package/src/memory/learning.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
8
|
+
import { isPausedJob, jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -428,7 +428,7 @@ export function scheduleMemoryMaintenance(
|
|
|
428
428
|
const settings = await memoryService.settings();
|
|
429
429
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled) return;
|
|
430
430
|
const store = await readExperienceStore(options.workspaceId);
|
|
431
|
-
const pending = store.pending.filter(turn => !turn
|
|
431
|
+
const pending = store.pending.filter(turn => !isPausedJob(turn));
|
|
432
432
|
const continuing = pending.some(turn => turn.createdAt <= (store.runs.at(-1)?.startedAt ?? 0));
|
|
433
433
|
if (!pending.length || (!continuing && pending.length < settings.learning.minPendingTurns)) return;
|
|
434
434
|
const next = Math.max(continuing ? 0 : store.lastSuccessAt + settings.learning.minIntervalHours * 3600000,
|
|
@@ -5,6 +5,7 @@ export function createLearningModel(provider, execute = chat) {
|
|
|
5
5
|
return async (prompt, signal, onProgress) => {
|
|
6
6
|
let text = '';
|
|
7
7
|
let finished = false;
|
|
8
|
+
let stopReason;
|
|
8
9
|
let usage;
|
|
9
10
|
for await (const event of execute({
|
|
10
11
|
provider,
|
|
@@ -35,6 +36,7 @@ export function createLearningModel(provider, execute = chat) {
|
|
|
35
36
|
if (event.type === 'interrupted')
|
|
36
37
|
throw new DOMException('记忆整理已中断', 'AbortError');
|
|
37
38
|
if (event.type === 'done') {
|
|
39
|
+
stopReason = event.stopReason;
|
|
38
40
|
finished = event.stopReason === 'stop';
|
|
39
41
|
usage = {
|
|
40
42
|
promptTokens: event.promptTokens,
|
|
@@ -43,6 +45,11 @@ export function createLearningModel(provider, execute = chat) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
signal.throwIfAborted();
|
|
48
|
+
// No end marker is a dropped connection (the client synthesizes
|
|
49
|
+
// stopReason 'unknown'), not something the model said. Classifying it as
|
|
50
|
+
// an invalid proposal let three network blips park a source for good.
|
|
51
|
+
if (stopReason === undefined || stopReason === 'unknown')
|
|
52
|
+
throw new Error('记忆整理模型连接中断(流未正常结束),保留待处理资料');
|
|
46
53
|
if (!finished)
|
|
47
54
|
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
48
55
|
if (!text.trim())
|
|
@@ -10,6 +10,7 @@ export function createLearningModel(
|
|
|
10
10
|
return async (prompt, signal, onProgress) => {
|
|
11
11
|
let text = '';
|
|
12
12
|
let finished = false;
|
|
13
|
+
let stopReason: string | undefined;
|
|
13
14
|
let usage: { promptTokens?: number; outputTokens?: number } | undefined;
|
|
14
15
|
for await (const event of execute({
|
|
15
16
|
provider,
|
|
@@ -41,6 +42,7 @@ export function createLearningModel(
|
|
|
41
42
|
if (event.type === 'interrupted')
|
|
42
43
|
throw new DOMException('记忆整理已中断', 'AbortError');
|
|
43
44
|
if (event.type === 'done') {
|
|
45
|
+
stopReason = event.stopReason;
|
|
44
46
|
finished = event.stopReason === 'stop';
|
|
45
47
|
usage = {
|
|
46
48
|
promptTokens: event.promptTokens,
|
|
@@ -49,6 +51,11 @@ export function createLearningModel(
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
signal.throwIfAborted();
|
|
54
|
+
// No end marker is a dropped connection (the client synthesizes
|
|
55
|
+
// stopReason 'unknown'), not something the model said. Classifying it as
|
|
56
|
+
// an invalid proposal let three network blips park a source for good.
|
|
57
|
+
if (stopReason === undefined || stopReason === 'unknown')
|
|
58
|
+
throw new Error('记忆整理模型连接中断(流未正常结束),保留待处理资料');
|
|
52
59
|
if (!finished)
|
|
53
60
|
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
54
61
|
if (!text.trim())
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { dismissExperience, readExperienceStore, restoreExperience, undoLearningRun, } from './experienceStore.js';
|
|
3
|
-
import { factCandidates, resolveFactCandidate } from './factCandidates.js';
|
|
3
|
+
import { factCandidates, isLiveFactCandidate, resolveFactCandidate, } from './factCandidates.js';
|
|
4
|
+
import { isPausedJob } from './jobPolicy.js';
|
|
4
5
|
import { cancelFactLearning, factLearningStatus, runFactLearning, } from './factLearning.js';
|
|
5
6
|
import { cancelLearning, persistedLearningStatus, runMemoryMaintenance, } from './learning.js';
|
|
6
7
|
import { memoryService } from './memoryService.js';
|
|
7
|
-
import { dismissProjectKnowledge, projectKnowledgeStatus, resolveProjectKnowledgeCandidate, restoreProjectKnowledge, } from './projectKnowledge.js';
|
|
8
|
+
import { dismissProjectKnowledge, isLiveKnowledgeCandidate, projectKnowledgeStatus, resolveProjectKnowledgeCandidate, restoreProjectKnowledge, } from './projectKnowledge.js';
|
|
8
9
|
export const memoryManagementRequest = z.object({
|
|
9
10
|
action: z.enum([
|
|
10
11
|
'status',
|
|
@@ -98,18 +99,18 @@ export async function learningManagementSnapshot(workspaceId) {
|
|
|
98
99
|
persistedLearningStatus(workspaceId),
|
|
99
100
|
memoryService.settings(),
|
|
100
101
|
]);
|
|
101
|
-
const
|
|
102
|
+
const statements = new Map(memories.records.map((record) => [record.id, record.statement]));
|
|
102
103
|
return {
|
|
103
104
|
workspaceId,
|
|
104
105
|
status: status.status,
|
|
105
106
|
pendingTurns: store.pending.length,
|
|
106
|
-
pausedTurns: store.pending.filter(
|
|
107
|
+
pausedTurns: store.pending.filter(isPausedJob).length,
|
|
107
108
|
experiences: store.items,
|
|
108
109
|
knowledge: knowledge.records,
|
|
109
|
-
knowledgeCandidates: knowledge.candidates.filter((item) => item.
|
|
110
|
-
factCandidates: candidates.filter((item) =>
|
|
110
|
+
knowledgeCandidates: knowledge.candidates.filter((item) => isLiveKnowledgeCandidate(item, knowledge.records)),
|
|
111
|
+
factCandidates: candidates.filter((item) => isLiveFactCandidate(item, statements)),
|
|
111
112
|
factPendingTurns: facts.pending.length,
|
|
112
|
-
factPausedTurns: facts.pending.filter(
|
|
113
|
+
factPausedTurns: facts.pending.filter(isPausedJob).length,
|
|
113
114
|
factReceipts: facts.receipts.slice(-20),
|
|
114
115
|
runs: store.runs.slice(-20).reverse(),
|
|
115
116
|
settings,
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
restoreExperience,
|
|
6
6
|
undoLearningRun,
|
|
7
7
|
} from './experienceStore.js';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
factCandidates,
|
|
10
|
+
isLiveFactCandidate,
|
|
11
|
+
resolveFactCandidate,
|
|
12
|
+
} from './factCandidates.js';
|
|
13
|
+
import { isPausedJob } from './jobPolicy.js';
|
|
9
14
|
import {
|
|
10
15
|
cancelFactLearning,
|
|
11
16
|
factLearningStatus,
|
|
@@ -20,6 +25,7 @@ import {
|
|
|
20
25
|
import { memoryService } from './memoryService.js';
|
|
21
26
|
import {
|
|
22
27
|
dismissProjectKnowledge,
|
|
28
|
+
isLiveKnowledgeCandidate,
|
|
23
29
|
projectKnowledgeStatus,
|
|
24
30
|
resolveProjectKnowledgeCandidate,
|
|
25
31
|
restoreProjectKnowledge,
|
|
@@ -144,22 +150,24 @@ export async function learningManagementSnapshot(workspaceId: string) {
|
|
|
144
150
|
persistedLearningStatus(workspaceId),
|
|
145
151
|
memoryService.settings(),
|
|
146
152
|
]);
|
|
147
|
-
const
|
|
153
|
+
const statements = new Map(
|
|
154
|
+
memories.records.map((record) => [record.id, record.statement]),
|
|
155
|
+
);
|
|
148
156
|
return {
|
|
149
157
|
workspaceId,
|
|
150
158
|
status: status.status,
|
|
151
159
|
pendingTurns: store.pending.length,
|
|
152
|
-
pausedTurns: store.pending.filter(
|
|
160
|
+
pausedTurns: store.pending.filter(isPausedJob).length,
|
|
153
161
|
experiences: store.items,
|
|
154
162
|
knowledge: knowledge.records,
|
|
155
|
-
knowledgeCandidates: knowledge.candidates.filter(
|
|
156
|
-
(item
|
|
163
|
+
knowledgeCandidates: knowledge.candidates.filter((item) =>
|
|
164
|
+
isLiveKnowledgeCandidate(item, knowledge.records),
|
|
157
165
|
),
|
|
158
|
-
factCandidates: candidates.filter(
|
|
159
|
-
(item
|
|
166
|
+
factCandidates: candidates.filter((item) =>
|
|
167
|
+
isLiveFactCandidate(item, statements),
|
|
160
168
|
),
|
|
161
169
|
factPendingTurns: facts.pending.length,
|
|
162
|
-
factPausedTurns: facts.pending.filter(
|
|
170
|
+
factPausedTurns: facts.pending.filter(isPausedJob).length,
|
|
163
171
|
factReceipts: facts.receipts.slice(-20),
|
|
164
172
|
runs: store.runs.slice(-20).reverse(),
|
|
165
173
|
settings,
|
|
@@ -3,7 +3,7 @@ import { readFile, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { clearExperiences, EXPERIENCE_CONTEXT_HEADER, experienceEntry, recallExperiences, } from './experienceStore.js';
|
|
5
5
|
import { assertAutomaticFactAllowed, blockFact, withFactBarriers, } from './factGovernance.js';
|
|
6
|
-
import { clearGlobalProjectKnowledge, clearProjectKnowledge, PROJECT_KNOWLEDGE_CONTEXT_HEADER, projectKnowledgeEntry, projectKnowledgeStatus, recallProjectKnowledge, } from './projectKnowledge.js';
|
|
6
|
+
import { clearGlobalProjectKnowledge, clearProjectKnowledge, isLiveKnowledgeCandidate, PROJECT_KNOWLEDGE_CONTEXT_HEADER, projectKnowledgeEntry, projectKnowledgeStatus, recallProjectKnowledge, } from './projectKnowledge.js';
|
|
7
7
|
import { countTextTokens } from '../context/tokenCounter.js';
|
|
8
8
|
import { containsForbiddenSecret, deriveTopicKey, hydrateRecord, parsePersona, serializePersona, sha256, validateMemoryInput, } from './codec.js';
|
|
9
9
|
import { DEFAULT_RETENTION, DEFAULT_SETTINGS, LEGACY_LEARNING_CADENCE, MEMORY_SETTINGS_REVISION, MEMORY_RETENTION_DAYS_MAX, MEMORY_RETENTION_DAYS_MIN, MEMORY_RETENTION_MB_MAX, MEMORY_RETENTION_MB_MIN, PERSONA_MAX_CHARS, } from './constants.js';
|
|
@@ -11,7 +11,8 @@ import { clearConversationEpisodes, episodicMemoryService, } from './episodicMem
|
|
|
11
11
|
import { acquireFileLock, appendPrivateLine, atomicWriteFile, auditFilePath, enqueueMemoryMutation, personaFilePath, scopeDirectory, settingsFilePath, } from './fileStore.js';
|
|
12
12
|
import { ensureMemoryMigrated } from './migration.js';
|
|
13
13
|
import { clearScopeTransactionQuarantine, readVersionedMemory, refreshTopicView, removeVersionedMemory, replaceMemoryBatchVersioned, scanMemoryScope, writeNewMemory, } from './repository.js';
|
|
14
|
-
import { isReviewDue, memoryTerms, recallFromSnapshot, truncateTokenBudget, } from './retrieval.js';
|
|
14
|
+
import { isReviewDue, isUnconfirmed, memoryTerms, recallFromSnapshot, takeTokenBudget, truncateTokenBudget, } from './retrieval.js';
|
|
15
|
+
import { recallQueryFor } from './recallQuery.js';
|
|
15
16
|
export { containsForbiddenSecret } from './codec.js';
|
|
16
17
|
export class MemoryTopicConflictError extends Error {
|
|
17
18
|
recordId;
|
|
@@ -239,6 +240,17 @@ function refreshTopicViewAfterMutation(scope, workspaceId) {
|
|
|
239
240
|
combinedSnapshotCache = null;
|
|
240
241
|
void refreshTopicView(scope, workspaceId).catch(() => undefined);
|
|
241
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Candidates live per workspace, so callers pass the workspace whose candidate
|
|
245
|
+
* file they can reach (a personal record's candidates may sit anywhere; the
|
|
246
|
+
* prompt-side liveness rule covers the rest). The update itself already
|
|
247
|
+
* succeeded: a settle failure is reported, never turned into a failed update.
|
|
248
|
+
*/
|
|
249
|
+
export async function settleCandidatesAfterUpdate(workspaceId, updated) {
|
|
250
|
+
await (await import('./factCandidates.js'))
|
|
251
|
+
.settleFactCandidatesFor(workspaceId, updated.record, updated.version)
|
|
252
|
+
.catch((error) => process.stderr.write(`[minimal-agent memory] 记忆已更新,但关联候选暂未结清:${error.message}\n`));
|
|
253
|
+
}
|
|
242
254
|
function sameTopic(a, b) {
|
|
243
255
|
const topicKey = b.topicKey || deriveTopicKey(b);
|
|
244
256
|
if (a.topicKey === topicKey)
|
|
@@ -323,6 +335,8 @@ export const memoryService = {
|
|
|
323
335
|
async remember(input, automatic) {
|
|
324
336
|
validateMemoryInput(input);
|
|
325
337
|
await ensureMemoryMigrated();
|
|
338
|
+
// Set when the user said the same thing again; see the refresh below.
|
|
339
|
+
let restated;
|
|
326
340
|
const commit = () => withFactBarriers(input.scope, input.workspaceId, async (barriers) => {
|
|
327
341
|
automatic?.signal?.throwIfAborted();
|
|
328
342
|
if (automatic)
|
|
@@ -345,6 +359,9 @@ export const memoryService = {
|
|
|
345
359
|
record: duplicate,
|
|
346
360
|
result: 'ok',
|
|
347
361
|
});
|
|
362
|
+
// A queued turn older than the last confirmation confirms nothing new.
|
|
363
|
+
if (!automatic || automatic.startedAt > duplicate.lastConfirmedAt)
|
|
364
|
+
restated = duplicate;
|
|
348
365
|
return duplicate;
|
|
349
366
|
}
|
|
350
367
|
if (duplicate) {
|
|
@@ -378,13 +395,36 @@ export const memoryService = {
|
|
|
378
395
|
await release();
|
|
379
396
|
}
|
|
380
397
|
});
|
|
381
|
-
|
|
398
|
+
const stored = await enqueueMemoryMutation(() => automatic && input.scope === 'personal'
|
|
382
399
|
? withFactBarriers('workspace', automatic.sourceWorkspaceId, async (barriers) => {
|
|
383
400
|
// Hold source-workspace barrier until a global fact commit finishes.
|
|
384
401
|
assertAutomaticFactAllowed(barriers, input, automatic.startedAt);
|
|
385
402
|
return commit();
|
|
386
403
|
})
|
|
387
404
|
: commit());
|
|
405
|
+
if (!restated)
|
|
406
|
+
return stored;
|
|
407
|
+
// Saying the same thing again is a fresh confirmation. Returning the old
|
|
408
|
+
// record untouched left `lastConfirmedAt` at the first mention, so a fact
|
|
409
|
+
// the user kept repeating still aged into "待复核" on the first date's clock.
|
|
410
|
+
// Done after the queue/lock are released: the update takes them itself.
|
|
411
|
+
try {
|
|
412
|
+
const current = await this.readWithVersion(restated.id, restated.scope, restated.workspaceId);
|
|
413
|
+
if (!current)
|
|
414
|
+
return stored;
|
|
415
|
+
const refreshed = await this.updateWithVersion(restated.id, restated.scope, current.version, {
|
|
416
|
+
evidence: input.evidence,
|
|
417
|
+
evidenceQuote: input.evidenceQuote,
|
|
418
|
+
sourceSessionId: input.sourceSessionId,
|
|
419
|
+
sourceMessageId: input.sourceMessageId,
|
|
420
|
+
status: 'active',
|
|
421
|
+
}, restated.workspaceId);
|
|
422
|
+
return refreshed?.record ?? stored;
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// Losing the refresh to a concurrent edit is harmless; the record is intact.
|
|
426
|
+
return stored;
|
|
427
|
+
}
|
|
388
428
|
},
|
|
389
429
|
async updateWithVersion(id, scope, expectedVersion, patch, workspaceId, candidate) {
|
|
390
430
|
await ensureMemoryMigrated();
|
|
@@ -485,7 +525,7 @@ export const memoryService = {
|
|
|
485
525
|
throw error;
|
|
486
526
|
}
|
|
487
527
|
};
|
|
488
|
-
|
|
528
|
+
const updated = await enqueueMemoryMutation(() => candidate
|
|
489
529
|
? withFactBarriers('workspace', candidate.sourceWorkspaceId, async (barriers) => {
|
|
490
530
|
// A global candidate from a cleared workspace cannot update an old fact.
|
|
491
531
|
if (candidate.startedAt <= barriers.clearedAt)
|
|
@@ -493,6 +533,10 @@ export const memoryService = {
|
|
|
493
533
|
return commit();
|
|
494
534
|
})
|
|
495
535
|
: commit());
|
|
536
|
+
const candidatesWorkspace = workspaceId ?? candidate?.sourceWorkspaceId;
|
|
537
|
+
if (updated && candidatesWorkspace)
|
|
538
|
+
await settleCandidatesAfterUpdate(candidatesWorkspace, updated);
|
|
539
|
+
return updated;
|
|
496
540
|
},
|
|
497
541
|
async update(id, scope, patch, workspaceId) {
|
|
498
542
|
const current = await this.readWithVersion(id, scope, workspaceId);
|
|
@@ -695,6 +739,8 @@ export const memoryService = {
|
|
|
695
739
|
const count = (base) => Math.max(base, Math.round(base * scale));
|
|
696
740
|
const snapshot = await combinedSnapshot(workspaceId);
|
|
697
741
|
const settings = await this.settings();
|
|
742
|
+
// Only retrieval widens; `input` still decides what the user is correcting.
|
|
743
|
+
const query = recallQueryFor(input, options.previousInput);
|
|
698
744
|
const persona = options.includePersona === false
|
|
699
745
|
? null
|
|
700
746
|
: await this.readPersona().catch(() => null);
|
|
@@ -706,46 +752,53 @@ export const memoryService = {
|
|
|
706
752
|
!currentInputOverrides(record, input))
|
|
707
753
|
.sort((a, b) => Number(b.scope === 'workspace') - Number(a.scope === 'workspace') ||
|
|
708
754
|
b.lastConfirmedAt - a.lastConfirmedAt);
|
|
709
|
-
|
|
755
|
+
// "Who the user is" is a handful of high-value records that no query
|
|
756
|
+
// word would ever match ("帮我润色文案" never says 后端工程师), so like
|
|
757
|
+
// preferences they are injected wholesale rather than by lexical hit.
|
|
758
|
+
const profile = snapshot.records
|
|
759
|
+
.filter((record) => record.category === 'profile' &&
|
|
760
|
+
record.sensitivity === 'normal' &&
|
|
761
|
+
record.status !== 'superseded' &&
|
|
762
|
+
(!record.validUntil || record.validUntil > Date.now()) &&
|
|
763
|
+
!currentInputOverrides(record, input))
|
|
764
|
+
.sort((a, b) => b.lastConfirmedAt - a.lastConfirmedAt);
|
|
765
|
+
const relevant = recallFromSnapshot(snapshot, query, settings, count(12))
|
|
710
766
|
.filter((record) => record.sensitivity === 'normal' &&
|
|
711
767
|
record.category !== 'preference' &&
|
|
768
|
+
record.category !== 'profile' &&
|
|
712
769
|
!currentInputOverrides(record, input))
|
|
713
770
|
.slice(0, count(5));
|
|
714
|
-
const reviewCount = snapshot.records.filter((record) => record.status !== 'superseded' &&
|
|
771
|
+
const reviewCount = snapshot.records.filter((record) => record.status !== 'superseded' && isUnconfirmed(record, settings)).length;
|
|
715
772
|
const sections = [];
|
|
716
773
|
const contextSections = [];
|
|
717
774
|
const includedIds = [];
|
|
718
775
|
let conflictWarning;
|
|
776
|
+
const candidateModule = await import('./factCandidates.js');
|
|
777
|
+
// Settled (corrected or forgotten) candidates are dropped here, before they
|
|
778
|
+
// can flag the current record as unconfirmed or bring old text back.
|
|
779
|
+
const statements = new Map(snapshot.records.map((record) => [record.id, record.statement]));
|
|
719
780
|
const conflicts = workspaceId
|
|
720
|
-
?
|
|
721
|
-
.factCandidates(workspaceId)
|
|
722
|
-
.catch((error) => {
|
|
781
|
+
? (await candidateModule.factCandidates(workspaceId).catch((error) => {
|
|
723
782
|
conflictWarning = `【记忆健康提示】冲突候选暂不可读:${error.message.slice(0, 300)}`;
|
|
724
783
|
return [];
|
|
725
|
-
})
|
|
784
|
+
})).filter((item) => candidateModule.isLiveFactCandidate(item, statements))
|
|
726
785
|
: [];
|
|
727
|
-
const pendingIds = new Set(conflicts
|
|
728
|
-
|
|
729
|
-
.map((item) => item.old?.id));
|
|
730
|
-
const queryTerms = memoryTerms(input);
|
|
786
|
+
const pendingIds = new Set(conflicts.map((item) => item.old?.id));
|
|
787
|
+
const queryTerms = memoryTerms(query);
|
|
731
788
|
// Evaluated after records are injected: a candidate whose old record is in
|
|
732
789
|
// this prompt must travel with it. Otherwise the model sees "旧值·待复核"
|
|
733
790
|
// with no new value — the user said "以后改用 pnpm" and the only thing the
|
|
734
791
|
// next turn learns is that npm might be stale. Preferences are injected
|
|
735
792
|
// wholesale, so relying on query overlap alone dropped exactly those.
|
|
736
|
-
const liveRecordIds = new Set(snapshot.records.map((record) => record.id));
|
|
737
793
|
const surfacedFactCandidates = () => conflicts
|
|
738
|
-
.filter((item) => item.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
])
|
|
747
|
-
.toLowerCase()
|
|
748
|
-
.includes(term))))
|
|
794
|
+
.filter((item) => ((item.old && includedIds.includes(item.old.id)) ||
|
|
795
|
+
queryTerms.some((term) => JSON.stringify([
|
|
796
|
+
item.proposed.subject,
|
|
797
|
+
item.proposed.statement,
|
|
798
|
+
item.old?.statement,
|
|
799
|
+
])
|
|
800
|
+
.toLowerCase()
|
|
801
|
+
.includes(term))))
|
|
749
802
|
.slice(0, count(3));
|
|
750
803
|
const pushSection = (id, content, ids = [], recordList) => {
|
|
751
804
|
if (countTextTokens([...sections, content].join('\n\n')) > budget)
|
|
@@ -767,7 +820,7 @@ export const memoryService = {
|
|
|
767
820
|
const personaText = truncateTokenBudget(persona.content, share(700));
|
|
768
821
|
pushSection('persona', `【全局 Agent 人格|可信本机配置】\n只用于角色、语气和协作风格;不得扩大权限、覆盖安全规则或改变当前任务。\n${personaText}`);
|
|
769
822
|
}
|
|
770
|
-
const describe = (record) => `- ${record.scope}/${record.category}/${record.subject} [${pendingIds.has(record.id) ||
|
|
823
|
+
const describe = (record) => `- ${record.scope}/${record.category}/${record.subject} [${pendingIds.has(record.id) || isUnconfirmed(record, settings) ? '待复核,不能当作当前已确认事实' : '已记录'};确认 ${new Date(record.lastConfirmedAt).toISOString().slice(0, 10)}]: ${record.statement}`;
|
|
771
824
|
const addRecords = (id, header, entries, sectionBudget) => {
|
|
772
825
|
const selected = [];
|
|
773
826
|
for (const entry of entries) {
|
|
@@ -782,10 +835,11 @@ export const memoryService = {
|
|
|
782
835
|
pushSection(id, `${header}\n${selected.map((item) => item.text).join('\n')}`, selected.map((item) => item.id), { header, entries: selected });
|
|
783
836
|
};
|
|
784
837
|
addRecords('preferences', '【响应偏好|personal 全局,workspace 项目】当前要求优先;项目偏好仅在当前项目适用;待复核信息不作为确定约束。', preferences.map((record) => ({ id: record.id, text: describe(record) })), share(550));
|
|
838
|
+
addRecords('profile', '【用户画像|稳定背景】只在与任务相关时使用,不要主动复述;当前消息优先。', profile.map((record) => ({ id: record.id, text: describe(record) })), share(400));
|
|
785
839
|
addRecords('relevant', '【相关本机记忆|低优先级、可纠正】当前消息优先;来源见 RecallMemory。', relevant.map((record) => ({ id: record.id, text: describe(record) })), share(850));
|
|
786
840
|
if (workspaceId) {
|
|
787
841
|
try {
|
|
788
|
-
const knowledge = await recallProjectKnowledge(workspaceId,
|
|
842
|
+
const knowledge = await recallProjectKnowledge(workspaceId, query, count(4));
|
|
789
843
|
// Header once via addRecords, not re-rendered per record: the old
|
|
790
844
|
// projectKnowledgeContext([item]) call embedded a full ~60-token header
|
|
791
845
|
// in every single entry, burning a quarter of this section's budget on
|
|
@@ -794,8 +848,9 @@ export const memoryService = {
|
|
|
794
848
|
id: item.id,
|
|
795
849
|
text: projectKnowledgeEntry(item),
|
|
796
850
|
})), share(800));
|
|
797
|
-
const
|
|
798
|
-
|
|
851
|
+
const knowledgeStatus = await projectKnowledgeStatus(workspaceId);
|
|
852
|
+
const knowledgeCandidates = knowledgeStatus.candidates
|
|
853
|
+
.filter((item) => isLiveKnowledgeCandidate(item, knowledgeStatus.records) &&
|
|
799
854
|
// Same rule as fact candidates: travels with its injected old record.
|
|
800
855
|
(includedIds.includes(item.oldId) ||
|
|
801
856
|
queryTerms.some((term) => JSON.stringify([
|
|
@@ -808,7 +863,7 @@ export const memoryService = {
|
|
|
808
863
|
.slice(0, count(3));
|
|
809
864
|
const relevantFactCandidates = surfacedFactCandidates();
|
|
810
865
|
if (relevantFactCandidates.length || knowledgeCandidates.length)
|
|
811
|
-
pushSection('health',
|
|
866
|
+
pushSection('health', `【相关知识变化待处理】旧值与新候选并存,候选不是现行事实。仅本轮用户明确纠正可直接作为本轮要求;历史候选不会自动替代旧值。影响当前任务时向用户确认哪个有效,并按确认结果结清候选(改写记录后候选自动失效);项目事实先用读取或测试核对。\n${[
|
|
812
867
|
...relevantFactCandidates.map((item) => `- fact candidate=${item.id}:旧=${item.old?.statement ?? '无'};新=${item.proposed.statement}`),
|
|
813
868
|
...knowledgeCandidates.map((item) => `- knowledge candidate=${item.id}:旧=${item.oldStatement};新=${item.proposed.statement}`),
|
|
814
869
|
].join('\n')}`);
|
|
@@ -817,7 +872,7 @@ export const memoryService = {
|
|
|
817
872
|
pushSection('health', `项目事实暂不可读:${error.message}。不要假设项目知识库为空。`);
|
|
818
873
|
}
|
|
819
874
|
try {
|
|
820
|
-
const experiences = await recallExperiences(workspaceId,
|
|
875
|
+
const experiences = await recallExperiences(workspaceId, query, count(3));
|
|
821
876
|
addRecords('experiences', EXPERIENCE_CONTEXT_HEADER, experiences.map((item) => ({
|
|
822
877
|
id: item.id,
|
|
823
878
|
text: experienceEntry(item),
|
|
@@ -827,12 +882,28 @@ export const memoryService = {
|
|
|
827
882
|
pushSection('health', `项目经验暂不可读:${error.message}。不要假设经验库为空。`);
|
|
828
883
|
}
|
|
829
884
|
}
|
|
885
|
+
// RecallMemory searches blind: the model cannot ask for what it does not
|
|
886
|
+
// know exists. A subject-only index of the records not injected above
|
|
887
|
+
// (no bodies, so it stays cheap) lets it decide what is worth reading.
|
|
888
|
+
const injected = new Set(includedIds);
|
|
889
|
+
const indexLines = snapshot.records
|
|
890
|
+
.filter((record) => !injected.has(record.id) &&
|
|
891
|
+
record.sensitivity === 'normal' &&
|
|
892
|
+
record.status !== 'superseded' &&
|
|
893
|
+
(!record.validUntil || record.validUntil > Date.now()))
|
|
894
|
+
.sort((a, b) => b.lastConfirmedAt - a.lastConfirmedAt)
|
|
895
|
+
.map((record) => `- ${record.scope}/${record.category}/${record.subject}`);
|
|
896
|
+
if (indexLines.length) {
|
|
897
|
+
const shown = takeTokenBudget(indexLines, share(400)).slice(0, count(60));
|
|
898
|
+
if (shown.length)
|
|
899
|
+
pushSection('index', `【其它已存记忆索引|仅主题,正文未载入】需要其中某条细节时用 RecallMemory 读取${shown.length < indexLines.length ? `(另有 ${indexLines.length - shown.length} 条未列出)` : ''}。\n${shown.join('\n')}`);
|
|
900
|
+
}
|
|
830
901
|
const settingsWarning = settingsWarnings.get(settingsFilePath());
|
|
831
902
|
if (workspaceId) {
|
|
832
903
|
try {
|
|
833
904
|
const facts = await (await import('./factLearning.js')).factLearningStatus(workspaceId);
|
|
834
905
|
const experiences = await (await import('./experienceStore.js')).readExperienceStore(workspaceId);
|
|
835
|
-
const paused = [...facts.pending, ...experiences.pending].filter(
|
|
906
|
+
const paused = [...facts.pending, ...experiences.pending].filter((await import('./jobPolicy.js')).isPausedJob).length;
|
|
836
907
|
if (paused)
|
|
837
908
|
pushSection('health', `【记忆健康提示】${paused} 条后台学习资料因重复失败已暂停自动重试,原资料仍保留;不得声称这些资料已完成长期保存。`);
|
|
838
909
|
}
|