wendkeep 0.68.1 → 0.68.6
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 +60 -0
- package/README.en.md +33 -12
- package/README.md +33 -12
- package/docs/en/commands/changes-and-verification.md +12 -0
- package/docs/en/commands/getting-started.md +12 -4
- package/docs/en/commands/memory.md +3 -1
- package/docs/en/commands/operating-profiles.md +11 -0
- package/docs/en/commands/sessions-and-import.md +15 -0
- package/docs/pt-BR/commands/changes-and-verification.md +12 -0
- package/docs/pt-BR/commands/getting-started.md +12 -4
- package/docs/pt-BR/commands/memory.md +3 -1
- package/docs/pt-BR/commands/operating-profiles.md +11 -0
- package/docs/pt-BR/commands/sessions-and-import.md +15 -0
- package/hooks/brain-core.mjs +159 -159
- package/hooks/brain-recall.mjs +32 -32
- package/hooks/brain-reindex.mjs +13 -13
- package/hooks/change-guard.mjs +142 -103
- package/hooks/git-snapshot.mjs +25 -6
- package/hooks/obsidian-common.mjs +40 -0
- package/hooks/project-scope.mjs +435 -0
- package/hooks/session-backfill.mjs +1 -1
- package/hooks/session-ensure.mjs +23 -4
- package/hooks/session-iteration-outcome.mjs +143 -0
- package/hooks/session-start.mjs +15 -0
- package/hooks/session-stop.mjs +263 -42
- package/hooks/token-usage.mjs +13 -11
- package/package.json +2 -2
- package/packages/integrations/src/host-hooks.mjs +7 -7
- package/packages/integrations/src/prompt-content.mjs +12 -0
- package/packages/integrations/src/transcripts.mjs +37 -8
- package/packages/vault/src/memory-store.mjs +63 -1
- package/src/init.mjs +3 -0
- package/src/memory.mjs +10 -3
package/hooks/change-guard.mjs
CHANGED
|
@@ -1,86 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// PreToolUse
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// R2 — `git commit` com change ativa E (--no-verify OU sensor crítico vermelho) vira `ask`
|
|
7
|
-
// (o usuário decide com 1 clique; falso-positivo custa pouco).
|
|
8
|
-
// Fast-path: comando sem wendkeep/wk/git sai sem NENHUM I/O. Ausência normal continua
|
|
9
|
-
// fail-open; corrupção do binding é diagnóstico visível e fail-closed.
|
|
2
|
+
// PreToolUse guard. R1 protects the explicit archive --force decision. The project-scope
|
|
3
|
+
// policy protects mutable Git/filesystem tools before they run, including Codex's raw-string
|
|
4
|
+
// and object-shaped tool_input variants. Host adapters never emit `ask` for Codex: the Codex
|
|
5
|
+
// contract accepts `deny` in PreToolUse, while Claude may still use `ask` for its own approval UI.
|
|
10
6
|
import { pathToFileURL } from 'node:url';
|
|
11
|
-
import { readHookInput, writeHookOutput } from './obsidian-common.mjs';
|
|
7
|
+
import { readHookInput, readSessionRegistry, writeHookOutput } from './obsidian-common.mjs';
|
|
12
8
|
import { activeChange, quickGateState } from './change-core.mjs';
|
|
13
9
|
import { hookProfilePolicy, resolveHookOperatingProfile } from './operating-profile-runtime.mjs';
|
|
14
10
|
import { isProjectVaultIntegrityError } from '../src/project-vault.mjs';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const GIT_EXECUTABLES = new Set(['git', 'git.exe', 'git.cmd']);
|
|
23
|
-
|
|
24
|
-
function shellSegments(command) {
|
|
25
|
-
const tokens = String(command || '').match(/"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|&&|\|\||[;|\n]|&|[^\s;&|]+/g) || [];
|
|
26
|
-
const segments = [];
|
|
27
|
-
let current = [];
|
|
28
|
-
const flush = () => {
|
|
29
|
-
if (current.length) segments.push(current);
|
|
30
|
-
current = [];
|
|
31
|
-
};
|
|
32
|
-
for (const token of tokens) {
|
|
33
|
-
if (['&&', '||', ';', '|', '\n'].includes(token) || (token === '&' && current.length)) {
|
|
34
|
-
flush();
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
current.push(token);
|
|
38
|
-
}
|
|
39
|
-
flush();
|
|
40
|
-
return segments;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function unquote(token) {
|
|
44
|
-
const value = String(token || '');
|
|
45
|
-
if (value.length >= 2 && ((value[0] === '"' && value.at(-1) === '"')
|
|
46
|
-
|| (value[0] === "'" && value.at(-1) === "'"))) return value.slice(1, -1);
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function executableName(token) {
|
|
51
|
-
return unquote(token).replaceAll('\\', '/').split('/').at(-1).toLowerCase();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function invocationOf(segment) {
|
|
55
|
-
let index = 0;
|
|
56
|
-
while (segment[index] === '&' || /^[A-Za-z_][A-Za-z0-9_]*=/.test(segment[index] || '')) index += 1;
|
|
57
|
-
const executable = executableName(segment[index]);
|
|
58
|
-
if (WK_EXECUTABLES.has(executable)) {
|
|
59
|
-
return { kind: 'wendkeep', args: segment.slice(index + 1).map(unquote) };
|
|
60
|
-
}
|
|
61
|
-
if (NODE_EXECUTABLES.has(executable)) {
|
|
62
|
-
let scriptIndex = index + 1;
|
|
63
|
-
while (String(segment[scriptIndex] || '').startsWith('-')) scriptIndex += 1;
|
|
64
|
-
if (executableName(segment[scriptIndex]) === 'wendkeep.mjs') {
|
|
65
|
-
return { kind: 'wendkeep', args: segment.slice(scriptIndex + 1).map(unquote) };
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (NPX_EXECUTABLES.has(executable)) {
|
|
69
|
-
let packageIndex = index + 1;
|
|
70
|
-
while (String(segment[packageIndex] || '').startsWith('-')) packageIndex += 1;
|
|
71
|
-
if (WK_EXECUTABLES.has(executableName(segment[packageIndex]))) {
|
|
72
|
-
return { kind: 'wendkeep', args: segment.slice(packageIndex + 1).map(unquote) };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (GIT_EXECUTABLES.has(executable)) {
|
|
76
|
-
return { kind: 'git', args: segment.slice(index + 1).map(unquote) };
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function commandInvocations(command) {
|
|
82
|
-
return shellSegments(command).map(invocationOf).filter(Boolean);
|
|
83
|
-
}
|
|
11
|
+
import {
|
|
12
|
+
captureProjectScope,
|
|
13
|
+
commandHasUnprovenTarget,
|
|
14
|
+
extractToolCommand,
|
|
15
|
+
scopeDecision,
|
|
16
|
+
scopeForRegistry,
|
|
17
|
+
} from './project-scope.mjs';
|
|
84
18
|
|
|
85
19
|
function bindingFailureDecision(diagnostic) {
|
|
86
20
|
const code = diagnostic?.code || 'WENDKEEP_VAULT_CONFIG_INVALID';
|
|
@@ -92,54 +26,150 @@ function bindingFailureDecision(diagnostic) {
|
|
|
92
26
|
};
|
|
93
27
|
}
|
|
94
28
|
|
|
95
|
-
|
|
29
|
+
function hostFor({ host, provider } = {}) {
|
|
30
|
+
if (host) return host;
|
|
31
|
+
if (provider === 'codex') return 'codex';
|
|
32
|
+
// Pure callers historically represented Claude's approval UI without a provider field.
|
|
33
|
+
return 'claude';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function scopeGuardDecision(command, {
|
|
37
|
+
input = {},
|
|
38
|
+
host,
|
|
39
|
+
provider,
|
|
40
|
+
expectedScope,
|
|
41
|
+
actualScope,
|
|
42
|
+
activeSessions = [],
|
|
43
|
+
projectRoot = '',
|
|
44
|
+
projectId = '',
|
|
45
|
+
sessionId = '',
|
|
46
|
+
} = {}) {
|
|
47
|
+
if (!expectedScope && input?.project_scope) expectedScope = input.project_scope;
|
|
48
|
+
if (!expectedScope && input?.projectScope) expectedScope = input.projectScope;
|
|
49
|
+
const actual = actualScope || captureProjectScope({
|
|
50
|
+
input,
|
|
51
|
+
projectRoot,
|
|
52
|
+
projectId,
|
|
53
|
+
provider,
|
|
54
|
+
sessionId,
|
|
55
|
+
});
|
|
56
|
+
const commandTargetKnown = !commandHasUnprovenTarget(command);
|
|
57
|
+
return scopeDecision({
|
|
58
|
+
command,
|
|
59
|
+
input,
|
|
60
|
+
expectedScope,
|
|
61
|
+
actualScope: actual,
|
|
62
|
+
host: hostFor({ host, provider }),
|
|
63
|
+
commandTargetKnown,
|
|
64
|
+
activeSessions,
|
|
65
|
+
currentSessionId: sessionId,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function guardDecision(command, {
|
|
70
|
+
vaultBase,
|
|
71
|
+
env = process.env,
|
|
72
|
+
profile = 'GOVERN',
|
|
73
|
+
input = {},
|
|
74
|
+
host,
|
|
75
|
+
provider,
|
|
76
|
+
expectedScope,
|
|
77
|
+
actualScope,
|
|
78
|
+
projectRoot = '',
|
|
79
|
+
projectId = '',
|
|
80
|
+
sessionId = '',
|
|
81
|
+
} = {}) {
|
|
96
82
|
if (!hookProfilePolicy(profile).harness) return null;
|
|
97
83
|
const cmd = String(command || '');
|
|
98
|
-
const invocations = commandInvocations(cmd);
|
|
99
|
-
if (!invocations.length) return null; // fast-path: parsing puro, zero I/O para o caso comum
|
|
100
84
|
|
|
101
|
-
// R1: archive --force — parser
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
permissionDecisionReason: '`change archive --force` é decisão do usuário, não sua. Gate vermelho = trabalho pendente: rode `wendkeep change status` e conclua as tarefas, ou `wendkeep change abandon <slug>` se a change não vai adiante. Se o usuário pediu o force explicitamente, peça a ele para rodar com WENDKEEP_ALLOW_FORCE=1.',
|
|
111
|
-
};
|
|
85
|
+
// R1: archive --force — parser/policy only, no scope is needed to deny the bypass.
|
|
86
|
+
if (/\b(?:wendkeep|wk)\b.*\bchange\s+archive\b.*--force(?:=|\b)/i.test(cmd)
|
|
87
|
+
|| /(?:wendkeep\.mjs|wendkeep\.cmd)\s+change\s+archive\b.*--force(?:=|\b)/i.test(cmd)) {
|
|
88
|
+
if (env.WENDKEEP_ALLOW_FORCE !== '1') {
|
|
89
|
+
return {
|
|
90
|
+
permissionDecision: 'deny',
|
|
91
|
+
permissionDecisionReason: '`change archive --force` é decisão do usuário, não sua. Gate vermelho = trabalho pendente: rode `wendkeep change status` e conclua as tarefas, ou `wendkeep change abandon <slug>` se a change não vai adiante. Se o usuário pediu o force explicitamente, peça a ele para rodar com WENDKEEP_ALLOW_FORCE=1.',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
112
94
|
}
|
|
113
95
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
96
|
+
const scoped = scopeGuardDecision(cmd, {
|
|
97
|
+
input,
|
|
98
|
+
host,
|
|
99
|
+
provider,
|
|
100
|
+
expectedScope,
|
|
101
|
+
actualScope,
|
|
102
|
+
projectRoot,
|
|
103
|
+
projectId,
|
|
104
|
+
sessionId,
|
|
105
|
+
});
|
|
106
|
+
if (scoped) return scoped;
|
|
107
|
+
|
|
108
|
+
// R2: commit with an active change and a bypass/red sensor. This branch is reached only
|
|
109
|
+
// after the project scope is proven. Codex gets deny because PreToolUse cannot ask.
|
|
110
|
+
if (/\bgit(?:\.exe|\.cmd)?\b[^;&|\n]*\bcommit\b/i.test(cmd)) {
|
|
118
111
|
const slug = activeChange(vaultBase);
|
|
119
112
|
if (!slug) return null;
|
|
120
|
-
const noVerify =
|
|
113
|
+
const noVerify = /(?:^|\s)--no-verify(?:=|\s|$)/i.test(cmd);
|
|
121
114
|
const gate = noVerify ? null : quickGateState(vaultBase);
|
|
122
115
|
if (noVerify || (gate && gate.redCritical)) {
|
|
116
|
+
const selectedHost = hostFor({ host, provider });
|
|
117
|
+
const reason = noVerify
|
|
118
|
+
? `git commit --no-verify com a change "${slug}" ativa — commitar pulando os hooks?`
|
|
119
|
+
: `A change ativa "${slug}" tem sensor crítico vermelho (wendkeep verify falhou). Commitar mesmo assim?`;
|
|
123
120
|
return {
|
|
124
|
-
permissionDecision: 'ask',
|
|
125
|
-
permissionDecisionReason:
|
|
126
|
-
?
|
|
127
|
-
:
|
|
121
|
+
permissionDecision: selectedHost === 'codex' ? 'deny' : 'ask',
|
|
122
|
+
permissionDecisionReason: selectedHost === 'codex'
|
|
123
|
+
? `${reason} O Codex exige uma autorização explícita fora do PreToolUse; execute a ação conscientemente após corrigir o gate.`
|
|
124
|
+
: reason,
|
|
128
125
|
};
|
|
129
126
|
}
|
|
130
127
|
}
|
|
131
128
|
return null;
|
|
132
129
|
}
|
|
133
130
|
|
|
131
|
+
function runtimeScope(runtime, input) {
|
|
132
|
+
const identity = runtime.identity || {};
|
|
133
|
+
const sessionId = identity.canonicalConversationId || input.session_id || input.sessionId || '';
|
|
134
|
+
const expected = runtime.entry?.project_scope
|
|
135
|
+
? {
|
|
136
|
+
...runtime.entry.project_scope,
|
|
137
|
+
conflict: runtime.entry.project_scope_conflict === true,
|
|
138
|
+
}
|
|
139
|
+
: null;
|
|
140
|
+
const actual = captureProjectScope({
|
|
141
|
+
input,
|
|
142
|
+
projectRoot: runtime.projectRoot || runtime.resolution?.projectRoot || '',
|
|
143
|
+
projectId: runtime.resolution?.projectId || '',
|
|
144
|
+
provider: identity.provider || 'codex',
|
|
145
|
+
sessionId,
|
|
146
|
+
});
|
|
147
|
+
const registry = readSessionRegistry(runtime.vaultBase);
|
|
148
|
+
const activeSessions = Object.entries(registry.sessions || {})
|
|
149
|
+
.filter(([, entry]) => entry?.status === 'active');
|
|
150
|
+
return { expectedScope: expected, actualScope: actual, activeSessions, sessionId };
|
|
151
|
+
}
|
|
152
|
+
|
|
134
153
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
135
154
|
try {
|
|
136
155
|
const input = readHookInput();
|
|
137
156
|
const runtime = resolveHookOperatingProfile({ input });
|
|
157
|
+
const command = extractToolCommand(input);
|
|
158
|
+
const scope = runtime.bindingError ? null : runtimeScope(runtime, input);
|
|
138
159
|
const d = runtime.bindingError
|
|
139
160
|
? bindingFailureDecision(runtime.bindingError)
|
|
140
|
-
: guardDecision(
|
|
161
|
+
: guardDecision(command, {
|
|
141
162
|
vaultBase: runtime.vaultBase,
|
|
142
163
|
profile: runtime.profile,
|
|
164
|
+
input,
|
|
165
|
+
provider: runtime.identity?.provider,
|
|
166
|
+
host: runtime.identity?.provider,
|
|
167
|
+
expectedScope: scope.expectedScope,
|
|
168
|
+
actualScope: scope.actualScope,
|
|
169
|
+
activeSessions: scope.activeSessions,
|
|
170
|
+
projectRoot: runtime.projectRoot,
|
|
171
|
+
projectId: runtime.resolution?.projectId,
|
|
172
|
+
sessionId: scope.sessionId,
|
|
143
173
|
});
|
|
144
174
|
if (d) writeHookOutput({ hookSpecificOutput: { hookEventName: 'PreToolUse', ...d } });
|
|
145
175
|
// allow implícito: exit 0 sem output
|
|
@@ -152,7 +182,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
152
182
|
},
|
|
153
183
|
});
|
|
154
184
|
} else {
|
|
155
|
-
|
|
185
|
+
// A malformed hook envelope cannot prove the target. Return a visible deny rather than
|
|
186
|
+
// silently permitting a mutable tool with unknown project scope.
|
|
187
|
+
writeHookOutput({
|
|
188
|
+
hookSpecificOutput: {
|
|
189
|
+
hookEventName: 'PreToolUse',
|
|
190
|
+
...bindingFailureDecision({ code: 'WENDKEEP_SCOPE_INPUT_INVALID', message: error.message }),
|
|
191
|
+
},
|
|
192
|
+
});
|
|
156
193
|
}
|
|
157
194
|
}
|
|
158
195
|
}
|
|
196
|
+
|
|
197
|
+
export { scopeForRegistry };
|
package/hooks/git-snapshot.mjs
CHANGED
|
@@ -31,6 +31,16 @@ function gitOptional(cwd, args, { spawn = spawnSync } = {}) {
|
|
|
31
31
|
return result.status === 0 ? String(result.stdout || '').trim() : '';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function parseGitConfigPaths(output) {
|
|
35
|
+
const values = new Map();
|
|
36
|
+
for (const line of String(output || '').split(/\r?\n/).filter(Boolean)) {
|
|
37
|
+
const separator = line.search(/\s/);
|
|
38
|
+
if (separator < 0) continue;
|
|
39
|
+
values.set(line.slice(0, separator).toLowerCase(), line.slice(separator + 1).trim());
|
|
40
|
+
}
|
|
41
|
+
return values;
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
function fingerprintFsEntry(path, unsafePaths = [], label = '') {
|
|
35
45
|
let stat;
|
|
36
46
|
try {
|
|
@@ -83,10 +93,16 @@ function fingerprintGitIndirection(path, unsafePaths) {
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
function gitMetadataSnapshot(root, options = {}) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
96
|
+
const [gitDirValue, commonDirValue] = String(git(root, [
|
|
97
|
+
'rev-parse', '--git-dir', '--git-common-dir',
|
|
98
|
+
], options)).trim().split(/\r?\n/);
|
|
99
|
+
const gitDir = resolveGitPath(root, gitDirValue);
|
|
100
|
+
const commonDir = resolveGitPath(root, commonDirValue);
|
|
101
|
+
const configuredPaths = parseGitConfigPaths(gitOptional(root, [
|
|
102
|
+
'config', '--path', '--get-regexp', '^core\\.(hooksPath|excludesFile)$',
|
|
103
|
+
], options));
|
|
104
|
+
const configuredHooks = configuredPaths.get('core.hookspath') || '';
|
|
105
|
+
const configuredExcludes = configuredPaths.get('core.excludesfile') || '';
|
|
90
106
|
const hooksPath = configuredHooks ? resolveGitPath(root, configuredHooks) : join(commonDir, 'hooks');
|
|
91
107
|
const unsafePaths = [];
|
|
92
108
|
const targets = [
|
|
@@ -508,7 +524,10 @@ function worktreeFingerprint(root, relPath, options = {}, diagnostics = {
|
|
|
508
524
|
|
|
509
525
|
export function captureGitSnapshot(projectRoot, options = {}) {
|
|
510
526
|
const start = resolve(projectRoot);
|
|
511
|
-
const
|
|
527
|
+
const [rootValue, headValue] = String(git(start, [
|
|
528
|
+
'rev-parse', '--show-toplevel', '--verify', 'HEAD',
|
|
529
|
+
], options)).trim().split(/\r?\n/);
|
|
530
|
+
const root = realpathSync.native(resolve(rootValue));
|
|
512
531
|
const canonicalRoot = canonicalFsPath(root);
|
|
513
532
|
const expectedRoot = options._expectedGitlinkRoot ? canonicalFsPath(options._expectedGitlinkRoot) : '';
|
|
514
533
|
if (expectedRoot && canonicalRoot !== expectedRoot) {
|
|
@@ -525,7 +544,7 @@ export function captureGitSnapshot(projectRoot, options = {}) {
|
|
|
525
544
|
}
|
|
526
545
|
seen.add(canonicalRoot);
|
|
527
546
|
options = { ...options, _gitlinkDepth: depth, _gitlinkSeen: [...seen] };
|
|
528
|
-
const head = String(
|
|
547
|
+
const head = String(headValue || '').trim();
|
|
529
548
|
const status = git(root, ['status', '--porcelain=v2', '-z', '--untracked-files=all'], { ...options, binary: true });
|
|
530
549
|
const fingerprints = {};
|
|
531
550
|
const dirtyPaths = new Set();
|
|
@@ -565,6 +565,46 @@ export function applyStopActivation(registry, stop = {}) {
|
|
|
565
565
|
return stopResult(next, 'applied', true);
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
// Finalization is a second causal transition: Stop first acknowledges the turn and publishes
|
|
569
|
+
// observability while the activation is still addressable, then closes that same activation. The
|
|
570
|
+
// explicit id check prevents a late Stop from closing a newer activation opened meanwhile.
|
|
571
|
+
export function closeSessionActivation(registry, stop = {}) {
|
|
572
|
+
const next = cloneRegistry(registry);
|
|
573
|
+
const sessionId = stop.session_id || stop.canonical_session_id || '';
|
|
574
|
+
const current = next.sessions[sessionId];
|
|
575
|
+
const requestedActivationId = String(stop.activation_id || '');
|
|
576
|
+
const activeId = String(current?.active_activation_id || '');
|
|
577
|
+
const stopTurnId = String(stop.turn_id || '');
|
|
578
|
+
if (!current || !requestedActivationId) return stopResult(next, 'ambiguous');
|
|
579
|
+
if (!activeId && current.status === 'done' && current.last_turn_id === stopTurnId) {
|
|
580
|
+
return stopResult(next, 'duplicate');
|
|
581
|
+
}
|
|
582
|
+
if (!activeId || activeId !== requestedActivationId) return stopResult(next, 'superseded');
|
|
583
|
+
|
|
584
|
+
const active = current.activations?.[activeId];
|
|
585
|
+
if (!active || active.status !== 'active') return stopResult(next, 'ambiguous');
|
|
586
|
+
|
|
587
|
+
const endedAt = String(stop.ended_at || '');
|
|
588
|
+
const activations = {
|
|
589
|
+
...(current.activations || {}),
|
|
590
|
+
[activeId]: {
|
|
591
|
+
...active,
|
|
592
|
+
status: 'done',
|
|
593
|
+
...(endedAt ? { ended_at: endedAt } : {}),
|
|
594
|
+
...(stopTurnId ? { last_stop_turn_id: stopTurnId } : {}),
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
next.sessions[sessionId] = {
|
|
598
|
+
...current,
|
|
599
|
+
status: 'done',
|
|
600
|
+
active_activation_id: '',
|
|
601
|
+
...(endedAt ? { ended_at: endedAt } : {}),
|
|
602
|
+
...(stopTurnId ? { last_turn_id: stopTurnId } : {}),
|
|
603
|
+
activations,
|
|
604
|
+
};
|
|
605
|
+
return stopResult(next, 'finalized', true);
|
|
606
|
+
}
|
|
607
|
+
|
|
568
608
|
// Remove one registry entry, but ONLY when its transcript matches the given path — this is
|
|
569
609
|
// self-healing for entries wendkeep itself mis-wrote (a subagent rollout registered as a
|
|
570
610
|
// top-level session by import <=0.46.1), never generic registry cleanup. An entry with the
|