wendkeep 0.66.3 → 0.66.5
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 +34 -0
- package/README.en.md +13 -8
- package/README.md +13 -8
- package/docs/en/commands/costs-and-observability.md +21 -7
- package/docs/en/commands/maintenance-and-diagnostics.md +13 -1
- package/docs/en/commands/memory.md +31 -2
- package/docs/en/commands/sessions-and-import.md +15 -0
- package/docs/pt-BR/commands/costs-and-observability.md +21 -7
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +12 -1
- package/docs/pt-BR/commands/memory.md +32 -2
- package/docs/pt-BR/commands/sessions-and-import.md +15 -1
- package/hooks/codex-rollout-meta.mjs +112 -0
- package/hooks/codex-subagent-graph.mjs +903 -0
- package/hooks/harness-doctor.mjs +82 -1
- package/hooks/import-sessions.mjs +185 -50
- package/hooks/session-identity.mjs +40 -5
- package/hooks/session-observability-lifecycle.mjs +129 -0
- package/hooks/session-observability-state.mjs +241 -0
- package/hooks/session-observability-store.mjs +436 -0
- package/hooks/session-observability.mjs +647 -21
- package/hooks/session-stop.mjs +218 -5
- package/hooks/subagent-stop.mjs +266 -12
- package/hooks/subagent-usage.mjs +65 -0
- package/hooks/token-usage.mjs +81 -4
- package/hooks/vault-health.mjs +6 -0
- package/package.json +1 -1
- package/packages/cli/src/index.mjs +1 -0
- package/packages/vault/src/memory-store.mjs +5 -0
- package/src/cost.mjs +40 -6
- package/src/doctor.mjs +4 -1
- package/src/memory.mjs +603 -5
- package/src/rebuild-costs.mjs +220 -34
package/hooks/session-stop.mjs
CHANGED
|
@@ -8,8 +8,13 @@ import { addUsage, costBreakdown, emptyTokenUsage, normalizeClaudeUsage, normali
|
|
|
8
8
|
import { buildBrainDigest, buildBrainIndex } from './brain-core.mjs';
|
|
9
9
|
import { activeChangeLink, pruneChangeSentinels } from './change-core.mjs';
|
|
10
10
|
import { getLocale } from './locale.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { materializeSessionObservability } from './session-observability.mjs';
|
|
12
12
|
import { resolveSessionEntry } from './session-identity.mjs';
|
|
13
|
+
import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
|
|
14
|
+
import {
|
|
15
|
+
markObservabilityCheckpoint,
|
|
16
|
+
readObservabilityStore,
|
|
17
|
+
} from './session-observability-store.mjs';
|
|
13
18
|
import { mutateSessionNote } from './session-note-io.mjs';
|
|
14
19
|
import { applyDerivedSections, provenanceSessions } from './derived-sections.mjs';
|
|
15
20
|
import { buildSessionMemoryEvents, collectLifecycleEvidence } from './memory-handoff.mjs';
|
|
@@ -794,7 +799,206 @@ export function shouldAbortStopAfterStaging(causalStop, memoryAttempt) {
|
|
|
794
799
|
);
|
|
795
800
|
}
|
|
796
801
|
|
|
797
|
-
|
|
802
|
+
const STOP_OBSERVABILITY_DEADLINE_MS = 45_000;
|
|
803
|
+
|
|
804
|
+
function stopEntryCausalSnapshot(entry) {
|
|
805
|
+
const activationId = String(entry?.active_activation_id || '');
|
|
806
|
+
const activation = entry?.activations?.[activationId] || {};
|
|
807
|
+
return {
|
|
808
|
+
activationId,
|
|
809
|
+
activationEpoch: Number(activation.epoch || entry?.activation_epoch || 0),
|
|
810
|
+
turnSequence: Number(entry?.last_turn_sequence || activation.last_turn_sequence || 0),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function expectedStopCausalSnapshot(entry, causalStop, turnSequence) {
|
|
815
|
+
if (!causalStop) return stopEntryCausalSnapshot(entry);
|
|
816
|
+
return {
|
|
817
|
+
activationId: String(causalStop.activationId || ''),
|
|
818
|
+
activationEpoch: Number(causalStop.activation?.epoch || entry?.activation_epoch || 0),
|
|
819
|
+
turnSequence: Number(turnSequence || 0),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function sameStopCausalSnapshot(left, right) {
|
|
824
|
+
return left.activationId === right.activationId
|
|
825
|
+
&& left.activationEpoch === right.activationEpoch
|
|
826
|
+
&& left.turnSequence === right.turnSequence;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function stopClaudeRoots(entry) {
|
|
830
|
+
const paths = new Set();
|
|
831
|
+
const add = (value) => {
|
|
832
|
+
if (typeof value === 'string' && value.trim()) paths.add(value.trim());
|
|
833
|
+
};
|
|
834
|
+
add(entry?.transcript_path);
|
|
835
|
+
for (const path of entry?.transcript_paths || []) add(path);
|
|
836
|
+
for (const activation of Object.values(entry?.activations || {})) {
|
|
837
|
+
add(activation?.transcript_path);
|
|
838
|
+
for (const path of activation?.transcript_paths || []) add(path);
|
|
839
|
+
}
|
|
840
|
+
return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function materializeStopObservability(request) {
|
|
844
|
+
return materializeSessionObservability({
|
|
845
|
+
vaultBase: request.vaultBase,
|
|
846
|
+
sessionPath: request.sessionPath,
|
|
847
|
+
transcriptPath: request.transcriptPath,
|
|
848
|
+
entry: request.entry,
|
|
849
|
+
canonicalConversationId: request.canonicalConversationId,
|
|
850
|
+
frontier: request.frontier,
|
|
851
|
+
signals: request.signals,
|
|
852
|
+
cache: request.cache,
|
|
853
|
+
mode: 'live',
|
|
854
|
+
deadlineAt: request.deadlineAt,
|
|
855
|
+
now: request.now,
|
|
856
|
+
allowNone: request.allowNone,
|
|
857
|
+
readRuntimeFrontier: request.readRuntimeFrontier,
|
|
858
|
+
withPublicationGuard: request.withPublicationGuard,
|
|
859
|
+
writeRegistryCheckpoint: request.writeRegistryCheckpoint,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export async function refreshStopObservability({
|
|
864
|
+
vaultBase,
|
|
865
|
+
input,
|
|
866
|
+
sessionPath,
|
|
867
|
+
sessionId,
|
|
868
|
+
entry,
|
|
869
|
+
causalStop,
|
|
870
|
+
turnSequence,
|
|
871
|
+
hookStartedAt,
|
|
872
|
+
expectedSignalSequence,
|
|
873
|
+
}, {
|
|
874
|
+
now = Date.now,
|
|
875
|
+
resolveEntry = resolveSessionEntry,
|
|
876
|
+
mutateRegistry = mutateSessionRegistry,
|
|
877
|
+
readStore = readObservabilityStore,
|
|
878
|
+
resolveRoots = resolveObservabilityRoots,
|
|
879
|
+
materialize = materializeStopObservability,
|
|
880
|
+
} = {}) {
|
|
881
|
+
if (causalStop && !causalStop.canPromoteMemory) return false;
|
|
882
|
+
const deadlineAt = hookStartedAt + STOP_OBSERVABILITY_DEADLINE_MS;
|
|
883
|
+
if (now() >= deadlineAt) return false;
|
|
884
|
+
|
|
885
|
+
const expected = expectedStopCausalSnapshot(entry, causalStop, turnSequence);
|
|
886
|
+
const fresh = resolveEntry(vaultBase, input, entry?.provider);
|
|
887
|
+
if (fresh.identity?.state !== 'resolved'
|
|
888
|
+
|| fresh.identity.canonicalConversationId !== sessionId
|
|
889
|
+
|| !fresh.entry?.session_file
|
|
890
|
+
|| fresh.entry.session_file !== entry?.session_file
|
|
891
|
+
|| !sameStopCausalSnapshot(expected, stopEntryCausalSnapshot(fresh.entry))) return false;
|
|
892
|
+
|
|
893
|
+
const runtime = readStore(vaultBase, sessionId);
|
|
894
|
+
const signalSequence = Number(runtime?.observability_signal_sequence || 0);
|
|
895
|
+
if (expectedSignalSequence !== undefined
|
|
896
|
+
&& signalSequence !== Number(expectedSignalSequence)) return false;
|
|
897
|
+
if (now() >= deadlineAt) return false;
|
|
898
|
+
|
|
899
|
+
const roots = fresh.identity.provider === 'codex'
|
|
900
|
+
? resolveRoots(fresh.entry)
|
|
901
|
+
: stopClaudeRoots(fresh.entry);
|
|
902
|
+
if (roots?.state !== 'complete' || !roots.rootPaths?.length) return false;
|
|
903
|
+
|
|
904
|
+
const frontier = {
|
|
905
|
+
canonical_session_id: sessionId,
|
|
906
|
+
activation_id: expected.activationId || 'legacy',
|
|
907
|
+
activation_epoch: expected.activationEpoch,
|
|
908
|
+
turn_sequence: expected.turnSequence,
|
|
909
|
+
signal_sequence: signalSequence,
|
|
910
|
+
roots_stat_hash: 'pending',
|
|
911
|
+
graph_cursor: 'pending',
|
|
912
|
+
source_manifest_hash: 'pending',
|
|
913
|
+
};
|
|
914
|
+
const readRuntimeFrontier = (candidateFrontier, guardContext) => {
|
|
915
|
+
const currentEntry = guardContext?.entry;
|
|
916
|
+
const current = currentEntry
|
|
917
|
+
? { identity: { state: 'resolved', canonicalConversationId: sessionId }, entry: currentEntry }
|
|
918
|
+
: resolveEntry(vaultBase, input, entry?.provider);
|
|
919
|
+
if (current.identity?.state !== 'resolved'
|
|
920
|
+
|| current.identity.canonicalConversationId !== sessionId
|
|
921
|
+
|| !current.entry) {
|
|
922
|
+
return { ...candidateFrontier, canonical_session_id: 'unresolved' };
|
|
923
|
+
}
|
|
924
|
+
const currentCausal = stopEntryCausalSnapshot(current.entry);
|
|
925
|
+
const currentRuntime = readStore(vaultBase, sessionId);
|
|
926
|
+
return {
|
|
927
|
+
...candidateFrontier,
|
|
928
|
+
activation_id: currentCausal.activationId || 'legacy',
|
|
929
|
+
activation_epoch: currentCausal.activationEpoch,
|
|
930
|
+
turn_sequence: currentCausal.turnSequence,
|
|
931
|
+
signal_sequence: Math.max(
|
|
932
|
+
Number(currentRuntime?.observability_signal_sequence || 0),
|
|
933
|
+
Number(current.entry.observability_signal_sequence || 0),
|
|
934
|
+
),
|
|
935
|
+
};
|
|
936
|
+
};
|
|
937
|
+
const withPublicationGuard = (_candidateFrontier, publishGuarded) => (
|
|
938
|
+
mutateRegistry(vaultBase, (registry) => publishGuarded({
|
|
939
|
+
registry,
|
|
940
|
+
entry: registry.sessions?.[sessionId] || null,
|
|
941
|
+
}))
|
|
942
|
+
);
|
|
943
|
+
const writeRegistryCheckpoint = ({
|
|
944
|
+
frontier: checkpointFrontier,
|
|
945
|
+
state,
|
|
946
|
+
diagnostics,
|
|
947
|
+
snapshot,
|
|
948
|
+
}, guardContext) => {
|
|
949
|
+
const registry = guardContext?.registry;
|
|
950
|
+
const current = registry?.sessions?.[sessionId];
|
|
951
|
+
if (!current) return null;
|
|
952
|
+
const currentSignal = Number(current.observability_signal_sequence || checkpointFrontier.signal_sequence);
|
|
953
|
+
registry.sessions[sessionId] = {
|
|
954
|
+
...current,
|
|
955
|
+
observability_signal_sequence: currentSignal,
|
|
956
|
+
observability_checkpoint_sequence: checkpointFrontier.signal_sequence,
|
|
957
|
+
observability_dirty: currentSignal > checkpointFrontier.signal_sequence,
|
|
958
|
+
observability_checkpoint_frontier: checkpointFrontier,
|
|
959
|
+
subagents_observability_state: state,
|
|
960
|
+
subagents_diagnostics: diagnostics || [],
|
|
961
|
+
};
|
|
962
|
+
return markObservabilityCheckpoint(vaultBase, sessionId, {
|
|
963
|
+
checkpointSequence: checkpointFrontier.signal_sequence,
|
|
964
|
+
frontier: checkpointFrontier,
|
|
965
|
+
sourceManifest: snapshot?.subagents?.sourceManifest,
|
|
966
|
+
graphCache: snapshot?.subagents?.cache,
|
|
967
|
+
diagnostics,
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
const result = await Promise.resolve(materialize({
|
|
972
|
+
vaultBase,
|
|
973
|
+
sessionPath,
|
|
974
|
+
entry: fresh.entry,
|
|
975
|
+
rootPaths: roots.rootPaths,
|
|
976
|
+
transcriptPath: roots.rootPaths[0],
|
|
977
|
+
caller: 'stop',
|
|
978
|
+
canonicalConversationId: sessionId,
|
|
979
|
+
activationId: expected.activationId,
|
|
980
|
+
activationEpoch: expected.activationEpoch,
|
|
981
|
+
turnSequence: expected.turnSequence,
|
|
982
|
+
signalSequence,
|
|
983
|
+
deadlineAt,
|
|
984
|
+
allowNone: true,
|
|
985
|
+
frontier,
|
|
986
|
+
signals: runtime?.signals || [],
|
|
987
|
+
cache: runtime?.graph_cache || null,
|
|
988
|
+
now,
|
|
989
|
+
readRuntimeFrontier,
|
|
990
|
+
withPublicationGuard,
|
|
991
|
+
writeRegistryCheckpoint,
|
|
992
|
+
}));
|
|
993
|
+
return !['stale', 'conflict', 'degraded', 'missing'].includes(result?.status);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export async function main({
|
|
997
|
+
stageMemory = stageStopMemoryAttempt,
|
|
998
|
+
clock = Date.now,
|
|
999
|
+
refreshObservability = refreshStopObservability,
|
|
1000
|
+
} = {}) {
|
|
1001
|
+
const hookStartedAt = clock();
|
|
798
1002
|
const input = readHookInput();
|
|
799
1003
|
if (input.stop_hook_active) {
|
|
800
1004
|
writeHookOutput({});
|
|
@@ -961,8 +1165,17 @@ export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
|
|
|
961
1165
|
}
|
|
962
1166
|
|
|
963
1167
|
try {
|
|
964
|
-
|
|
965
|
-
vaultBase,
|
|
1168
|
+
await refreshObservability({
|
|
1169
|
+
vaultBase,
|
|
1170
|
+
input,
|
|
1171
|
+
sessionPath,
|
|
1172
|
+
sessionId,
|
|
1173
|
+
entry,
|
|
1174
|
+
causalStop,
|
|
1175
|
+
turnSequence: stopTurnSequence,
|
|
1176
|
+
hookStartedAt,
|
|
1177
|
+
}, {
|
|
1178
|
+
now: clock,
|
|
966
1179
|
});
|
|
967
1180
|
} catch (error) {
|
|
968
1181
|
process.stderr.write(`[wendkeep] Token usage falhou: ${error.message}\n`);
|
|
@@ -1062,7 +1275,7 @@ export function main({ stageMemory = stageStopMemoryAttempt } = {}) {
|
|
|
1062
1275
|
|
|
1063
1276
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1064
1277
|
try {
|
|
1065
|
-
main();
|
|
1278
|
+
await main();
|
|
1066
1279
|
} catch (error) {
|
|
1067
1280
|
process.stderr.write(`[wendkeep] Stop falhou: ${error.message}\n`);
|
|
1068
1281
|
// Same reasoning as the identity bail: stderr is discarded by Codex. Exit stays 0 —
|
package/hooks/subagent-stop.mjs
CHANGED
|
@@ -8,29 +8,283 @@
|
|
|
8
8
|
import { existsSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { pathToFileURL } from 'url';
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
getVaultBase,
|
|
13
|
+
mutateSessionRegistry,
|
|
14
|
+
providerMeta,
|
|
15
|
+
readHookInput,
|
|
16
|
+
writeHookOutput,
|
|
17
|
+
} from './obsidian-common.mjs';
|
|
18
|
+
import { materializeSessionObservability } from './session-observability.mjs';
|
|
13
19
|
import { resolveSessionEntry } from './session-identity.mjs';
|
|
20
|
+
import { readCodexRolloutMeta } from './codex-rollout-meta.mjs';
|
|
21
|
+
import { resolveObservabilityRoots } from './session-observability-lifecycle.mjs';
|
|
22
|
+
import {
|
|
23
|
+
readObservabilityStore,
|
|
24
|
+
markObservabilityCheckpoint,
|
|
25
|
+
recordObservabilitySignal,
|
|
26
|
+
releaseObservabilityLease,
|
|
27
|
+
tryAcquireObservabilityLease,
|
|
28
|
+
} from './session-observability-store.mjs';
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
const SUBAGENT_COALESCE_MS = 250;
|
|
31
|
+
const SUBAGENT_DEADLINE_MS = 15_000;
|
|
32
|
+
|
|
33
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
+
|
|
35
|
+
function causalSnapshot(entry) {
|
|
36
|
+
const activationId = String(entry?.active_activation_id || '');
|
|
37
|
+
const activation = entry?.activations?.[activationId] || {};
|
|
38
|
+
return {
|
|
39
|
+
activationId,
|
|
40
|
+
activationEpoch: Number(activation.epoch || entry?.activation_epoch || 0),
|
|
41
|
+
turnSequence: Number(entry?.last_turn_sequence || activation.last_turn_sequence || 0),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sameCausalSnapshot(left, right) {
|
|
46
|
+
return left.activationId === right.activationId
|
|
47
|
+
&& left.activationEpoch === right.activationEpoch
|
|
48
|
+
&& left.turnSequence === right.turnSequence;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function defaultMaterialize(request) {
|
|
52
|
+
return materializeSessionObservability({
|
|
53
|
+
vaultBase: request.vaultBase,
|
|
54
|
+
sessionPath: request.sessionPath,
|
|
55
|
+
transcriptPath: request.transcriptPath,
|
|
56
|
+
entry: request.entry,
|
|
57
|
+
canonicalConversationId: request.canonicalConversationId,
|
|
58
|
+
frontier: request.frontier,
|
|
59
|
+
signals: request.signals,
|
|
60
|
+
cache: request.cache,
|
|
61
|
+
mode: 'live',
|
|
62
|
+
deadlineAt: request.deadlineAt,
|
|
63
|
+
now: request.now,
|
|
64
|
+
allowNone: request.allowNone,
|
|
65
|
+
readRuntimeFrontier: request.readRuntimeFrontier,
|
|
66
|
+
withPublicationGuard: request.withPublicationGuard,
|
|
67
|
+
writeRegistryCheckpoint: request.writeRegistryCheckpoint,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function claudeRoots(entry) {
|
|
72
|
+
const paths = new Set();
|
|
73
|
+
const add = (value) => {
|
|
74
|
+
if (typeof value === 'string' && value.trim()) paths.add(value.trim());
|
|
75
|
+
};
|
|
76
|
+
add(entry?.transcript_path);
|
|
77
|
+
for (const path of entry?.transcript_paths || []) add(path);
|
|
78
|
+
for (const activation of Object.values(entry?.activations || {})) {
|
|
79
|
+
add(activation?.transcript_path);
|
|
80
|
+
for (const path of activation?.transcript_paths || []) add(path);
|
|
81
|
+
}
|
|
82
|
+
return { state: 'complete', rootPaths: [...paths], descendantPaths: [], diagnostics: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function refreshSubagents(vaultBase, input, {
|
|
86
|
+
now = Date.now,
|
|
87
|
+
hookStartedAt = now(),
|
|
88
|
+
sleep = wait,
|
|
89
|
+
coalesceMs = SUBAGENT_COALESCE_MS,
|
|
90
|
+
deadlineMs = SUBAGENT_DEADLINE_MS,
|
|
91
|
+
resolveEntry = resolveSessionEntry,
|
|
92
|
+
readMeta = readCodexRolloutMeta,
|
|
93
|
+
mutateRegistry = mutateSessionRegistry,
|
|
94
|
+
recordSignal = recordObservabilitySignal,
|
|
95
|
+
readStore = readObservabilityStore,
|
|
96
|
+
acquireLease = tryAcquireObservabilityLease,
|
|
97
|
+
releaseLease = releaseObservabilityLease,
|
|
98
|
+
resolveRoots = resolveObservabilityRoots,
|
|
99
|
+
materialize = defaultMaterialize,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const deadlineAt = hookStartedAt + deadlineMs;
|
|
102
|
+
const provider = providerMeta(input.provider).id;
|
|
103
|
+
const { identity, entry } = resolveEntry(vaultBase, input, provider);
|
|
17
104
|
if (identity.state !== 'resolved') return false;
|
|
18
|
-
const
|
|
105
|
+
const childTranscriptPath = identity.transcriptPath;
|
|
19
106
|
const sessionRel = entry?.session_file || '';
|
|
20
107
|
if (!sessionRel) return false;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
108
|
+
|
|
109
|
+
let childParentThreadId = '';
|
|
110
|
+
if (identity.provider === 'codex') {
|
|
111
|
+
const childMeta = readMeta(childTranscriptPath);
|
|
112
|
+
if (!childMeta?.ok || !childMeta.meta?.source?.subagent) return false;
|
|
113
|
+
if (!childMeta.meta.id || childMeta.meta.id !== identity.transcriptId) return false;
|
|
114
|
+
if (childMeta.meta.session_id
|
|
115
|
+
&& childMeta.meta.session_id !== identity.canonicalConversationId) return false;
|
|
116
|
+
childParentThreadId = String(
|
|
117
|
+
childMeta.meta.parent_thread_id
|
|
118
|
+
|| childMeta.meta.source?.subagent?.thread_spawn?.parent_thread_id
|
|
119
|
+
|| '',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const observed = causalSnapshot(entry);
|
|
124
|
+
const signal = mutateRegistry(vaultBase, (registry) => {
|
|
125
|
+
const current = registry.sessions?.[identity.canonicalConversationId];
|
|
126
|
+
if (!current || current.session_file !== sessionRel
|
|
127
|
+
|| !sameCausalSnapshot(observed, causalSnapshot(current))) return null;
|
|
128
|
+
const recorded = recordSignal(vaultBase, identity.canonicalConversationId, {
|
|
129
|
+
rollout_id: identity.transcriptId,
|
|
130
|
+
transcript_path: childTranscriptPath,
|
|
131
|
+
parent_thread_id: childParentThreadId,
|
|
132
|
+
kind: 'started',
|
|
133
|
+
activation_id: observed.activationId,
|
|
134
|
+
activation_epoch: observed.activationEpoch,
|
|
135
|
+
turn_sequence: observed.turnSequence,
|
|
136
|
+
});
|
|
137
|
+
if (!recorded?.state) return recorded;
|
|
138
|
+
registry.sessions[identity.canonicalConversationId] = {
|
|
139
|
+
...current,
|
|
140
|
+
observability_signal_sequence: recorded.sequence,
|
|
141
|
+
observability_checkpoint_sequence: Number(
|
|
142
|
+
recorded.state.observability_checkpoint_sequence
|
|
143
|
+
?? current.observability_checkpoint_sequence
|
|
144
|
+
?? 0,
|
|
145
|
+
),
|
|
146
|
+
observability_dirty: Boolean(recorded.state.observability_dirty),
|
|
147
|
+
};
|
|
148
|
+
return recorded;
|
|
149
|
+
});
|
|
150
|
+
if (!signal?.state) return false;
|
|
151
|
+
if (!signal.state.observability_dirty) return true;
|
|
152
|
+
|
|
153
|
+
await sleep(coalesceMs);
|
|
154
|
+
if (now() >= deadlineAt) return true;
|
|
155
|
+
|
|
156
|
+
const latest = readStore(vaultBase, identity.canonicalConversationId);
|
|
157
|
+
if (!latest?.observability_dirty
|
|
158
|
+
|| latest.observability_signal_sequence !== signal.sequence) return true;
|
|
159
|
+
|
|
160
|
+
const leaseNow = now();
|
|
161
|
+
if (leaseNow >= deadlineAt) return true;
|
|
162
|
+
const lease = acquireLease(vaultBase, identity.canonicalConversationId, {
|
|
163
|
+
signalSequence: signal.sequence,
|
|
164
|
+
now: leaseNow,
|
|
165
|
+
ttlMs: Math.max(1, deadlineAt - leaseNow),
|
|
26
166
|
});
|
|
27
|
-
return true;
|
|
167
|
+
if (!lease?.acquired) return true;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
if (now() >= deadlineAt) return true;
|
|
171
|
+
const fresh = resolveEntry(vaultBase, input, provider);
|
|
172
|
+
if (fresh.identity?.state !== 'resolved'
|
|
173
|
+
|| fresh.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
174
|
+
|| !fresh.entry?.session_file
|
|
175
|
+
|| !sameCausalSnapshot(observed, causalSnapshot(fresh.entry))) return true;
|
|
176
|
+
|
|
177
|
+
const sessionPath = join(vaultBase, fresh.entry.session_file);
|
|
178
|
+
if (!existsSync(sessionPath)) return true;
|
|
179
|
+
const roots = identity.provider === 'codex'
|
|
180
|
+
? resolveRoots(fresh.entry)
|
|
181
|
+
: claudeRoots(fresh.entry);
|
|
182
|
+
if (roots?.state !== 'complete' || !roots.rootPaths?.length) return true;
|
|
183
|
+
|
|
184
|
+
const runtimeState = lease.state || latest;
|
|
185
|
+
const frontier = {
|
|
186
|
+
canonical_session_id: identity.canonicalConversationId,
|
|
187
|
+
activation_id: observed.activationId || 'legacy',
|
|
188
|
+
activation_epoch: observed.activationEpoch,
|
|
189
|
+
turn_sequence: observed.turnSequence,
|
|
190
|
+
signal_sequence: signal.sequence,
|
|
191
|
+
roots_stat_hash: 'pending',
|
|
192
|
+
graph_cursor: 'pending',
|
|
193
|
+
source_manifest_hash: 'pending',
|
|
194
|
+
};
|
|
195
|
+
const readRuntimeFrontier = (candidateFrontier, guardContext) => {
|
|
196
|
+
const currentRuntime = readStore(vaultBase, identity.canonicalConversationId);
|
|
197
|
+
const currentEntry = guardContext?.entry;
|
|
198
|
+
const currentResolved = currentEntry
|
|
199
|
+
? { identity: { state: 'resolved', canonicalConversationId: identity.canonicalConversationId }, entry: currentEntry }
|
|
200
|
+
: resolveEntry(vaultBase, input, provider);
|
|
201
|
+
if (currentResolved.identity?.state !== 'resolved'
|
|
202
|
+
|| currentResolved.identity.canonicalConversationId !== identity.canonicalConversationId
|
|
203
|
+
|| !currentResolved.entry) {
|
|
204
|
+
return { ...candidateFrontier, canonical_session_id: 'unresolved' };
|
|
205
|
+
}
|
|
206
|
+
const currentCausal = causalSnapshot(currentResolved.entry);
|
|
207
|
+
return {
|
|
208
|
+
...candidateFrontier,
|
|
209
|
+
activation_id: currentCausal.activationId || 'legacy',
|
|
210
|
+
activation_epoch: currentCausal.activationEpoch,
|
|
211
|
+
turn_sequence: currentCausal.turnSequence,
|
|
212
|
+
signal_sequence: Math.max(
|
|
213
|
+
Number(currentRuntime?.observability_signal_sequence || 0),
|
|
214
|
+
Number(currentResolved.entry.observability_signal_sequence || 0),
|
|
215
|
+
),
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
const withPublicationGuard = (_candidateFrontier, publishGuarded) => (
|
|
219
|
+
mutateRegistry(vaultBase, (registry) => publishGuarded({
|
|
220
|
+
registry,
|
|
221
|
+
entry: registry.sessions?.[identity.canonicalConversationId] || null,
|
|
222
|
+
}))
|
|
223
|
+
);
|
|
224
|
+
const writeRegistryCheckpoint = ({
|
|
225
|
+
frontier: checkpointFrontier,
|
|
226
|
+
state,
|
|
227
|
+
diagnostics,
|
|
228
|
+
snapshot,
|
|
229
|
+
}, guardContext) => {
|
|
230
|
+
const registry = guardContext?.registry;
|
|
231
|
+
const current = registry?.sessions?.[identity.canonicalConversationId];
|
|
232
|
+
if (!current) return null;
|
|
233
|
+
const currentSignal = Number(current.observability_signal_sequence || checkpointFrontier.signal_sequence);
|
|
234
|
+
registry.sessions[identity.canonicalConversationId] = {
|
|
235
|
+
...current,
|
|
236
|
+
observability_signal_sequence: currentSignal,
|
|
237
|
+
observability_checkpoint_sequence: checkpointFrontier.signal_sequence,
|
|
238
|
+
observability_dirty: currentSignal > checkpointFrontier.signal_sequence,
|
|
239
|
+
observability_checkpoint_frontier: checkpointFrontier,
|
|
240
|
+
subagents_observability_state: state,
|
|
241
|
+
subagents_diagnostics: diagnostics || [],
|
|
242
|
+
};
|
|
243
|
+
return markObservabilityCheckpoint(vaultBase, identity.canonicalConversationId, {
|
|
244
|
+
checkpointSequence: checkpointFrontier.signal_sequence,
|
|
245
|
+
frontier: checkpointFrontier,
|
|
246
|
+
sourceManifest: snapshot?.subagents?.sourceManifest,
|
|
247
|
+
graphCache: snapshot?.subagents?.cache,
|
|
248
|
+
diagnostics,
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await Promise.resolve(materialize({
|
|
253
|
+
vaultBase,
|
|
254
|
+
sessionPath,
|
|
255
|
+
entry: fresh.entry,
|
|
256
|
+
rootPaths: roots.rootPaths,
|
|
257
|
+
transcriptPath: roots.rootPaths[0],
|
|
258
|
+
caller: 'subagent-stop',
|
|
259
|
+
canonicalConversationId: identity.canonicalConversationId,
|
|
260
|
+
activationId: observed.activationId,
|
|
261
|
+
activationEpoch: observed.activationEpoch,
|
|
262
|
+
turnSequence: observed.turnSequence,
|
|
263
|
+
signalSequence: signal.sequence,
|
|
264
|
+
deadlineAt,
|
|
265
|
+
allowNone: false,
|
|
266
|
+
frontier,
|
|
267
|
+
signals: runtimeState.signals || [],
|
|
268
|
+
cache: runtimeState.graph_cache || null,
|
|
269
|
+
now,
|
|
270
|
+
readRuntimeFrontier,
|
|
271
|
+
withPublicationGuard,
|
|
272
|
+
writeRegistryCheckpoint,
|
|
273
|
+
}));
|
|
274
|
+
return true;
|
|
275
|
+
} finally {
|
|
276
|
+
releaseLease(vaultBase, identity.canonicalConversationId, {
|
|
277
|
+
ownerToken: lease.ownerToken,
|
|
278
|
+
signalSequence: signal.sequence,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
28
281
|
}
|
|
29
282
|
|
|
30
283
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
31
284
|
try {
|
|
285
|
+
const hookStartedAt = Date.now();
|
|
32
286
|
const input = readHookInput();
|
|
33
|
-
refreshSubagents(getVaultBase(input), input);
|
|
287
|
+
await refreshSubagents(getVaultBase(input), input, { hookStartedAt });
|
|
34
288
|
writeHookOutput({});
|
|
35
289
|
} catch (error) {
|
|
36
290
|
process.stderr.write(`[wendkeep] subagent-stop falhou: ${error.message}\n`);
|
package/hooks/subagent-usage.mjs
CHANGED
|
@@ -29,6 +29,49 @@ function walkAgentJsonl(dir) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function inspectAgentJsonlDirectory(dir) {
|
|
33
|
+
let names;
|
|
34
|
+
try {
|
|
35
|
+
names = readdirSync(dir);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { state: error?.code === 'ENOENT' ? 'absent' : 'error', files: [] };
|
|
38
|
+
}
|
|
39
|
+
const files = [];
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
const path = join(dir, name);
|
|
42
|
+
let stat;
|
|
43
|
+
try {
|
|
44
|
+
stat = statSync(path);
|
|
45
|
+
} catch {
|
|
46
|
+
return { state: 'error', files: [] };
|
|
47
|
+
}
|
|
48
|
+
if (stat.isDirectory()) {
|
|
49
|
+
const nested = inspectAgentJsonlDirectory(path);
|
|
50
|
+
if (nested.state === 'error' || nested.state === 'absent') return { state: 'error', files: [] };
|
|
51
|
+
files.push(...nested.files);
|
|
52
|
+
} else if (name.startsWith('agent-') && name.endsWith('.jsonl')) {
|
|
53
|
+
files.push(path);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { state: 'ok', files };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validJsonl(path) {
|
|
60
|
+
let lines;
|
|
61
|
+
try {
|
|
62
|
+
lines = readFileSync(path, 'utf8').split(/\r?\n/).filter((line) => line.trim());
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (!lines.length) return false;
|
|
67
|
+
try {
|
|
68
|
+
for (const line of lines) JSON.parse(line);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
32
75
|
// workflows/scripts/<name>-wf_<rid>.js -> { wf_<rid>: <name> }
|
|
33
76
|
function workflowNameMap(sessionDir) {
|
|
34
77
|
const map = {};
|
|
@@ -276,6 +319,28 @@ export function collectSubagentUsage(sessionDir) {
|
|
|
276
319
|
};
|
|
277
320
|
}
|
|
278
321
|
|
|
322
|
+
// Schema-2 callers need to distinguish proven absence from read/parse failure. Keep the
|
|
323
|
+
// legacy aggregate API above intact while exposing a fail-closed state for observability.
|
|
324
|
+
export function collectClaudeSubagentUsageState(sessionDir) {
|
|
325
|
+
const scan = inspectAgentJsonlDirectory(join(sessionDir, 'subagents'));
|
|
326
|
+
if (scan.state === 'absent' || (scan.state === 'ok' && scan.files.length === 0)) {
|
|
327
|
+
return { state: 'none', diagnostics: [] };
|
|
328
|
+
}
|
|
329
|
+
if (scan.state !== 'ok' || scan.files.some((path) => !validJsonl(path))) {
|
|
330
|
+
return {
|
|
331
|
+
state: 'degraded',
|
|
332
|
+
diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const collected = collectSubagentUsage(sessionDir);
|
|
336
|
+
return collected
|
|
337
|
+
? { ...collected, state: 'complete', diagnostics: [] }
|
|
338
|
+
: {
|
|
339
|
+
state: 'degraded',
|
|
340
|
+
diagnostics: [{ code: 'CHILD_META_INVALID', count: 1 }],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
279
344
|
function workflowLine(w) {
|
|
280
345
|
const parts = [w.runId];
|
|
281
346
|
if (w.status) parts.push(w.status);
|