blun-king-cli 9.1.597 → 9.1.600

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.
Files changed (106) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +17 -1
  3. package/agent-spine-plugin/CHANGELOG.md +1 -1
  4. package/agent-spine-plugin/docs/host-integration.md +1 -15
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install.js +1 -6
  7. package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
  8. package/agent-spine-plugin/scripts/release-check.js +2 -3
  9. package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
  10. package/agent-spine-plugin/src/hook.js +40 -40
  11. package/agent-spine-plugin/src/lib/briefing.js +6 -3
  12. package/agent-spine-plugin/src/lib/hook-context.js +63 -64
  13. package/agent-spine-plugin/src/lib/hook-output.js +115 -94
  14. package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
  15. package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
  16. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
  17. package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
  18. package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
  19. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
  20. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
  21. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
  22. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
  23. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
  24. package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
  25. package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
  26. package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
  27. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
  28. package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
  29. package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
  30. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
  31. package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
  32. package/agent-spine-plugin/src/lib/source-roots.js +26 -16
  33. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
  34. package/bin/core-bootstrap.js +18 -10
  35. package/bin/launcher-mode.js +5 -0
  36. package/bin/launcher-runtime.js +27 -0
  37. package/bin/verify-bundled-agent-sources.cjs +28 -2
  38. package/blun.mjs +798 -143
  39. package/bundled-agent-sources.json +192 -155
  40. package/package.json +1 -1
  41. package/standard-skills/translate-native/README.md +1334 -1
  42. package/standard-skills/translate-native/SKILL.md +1 -1
  43. package/standard-skills/translate-native/VERSION +1 -1
  44. package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
  45. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
  46. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
  47. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
  48. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
  49. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
  50. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
  51. package/standard-skills/translate-native/integrations/website_localization.py +234 -10
  52. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
  53. package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
  54. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
  55. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
  56. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
  57. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
  58. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
  59. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
  60. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
  61. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
  62. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
  63. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
  64. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
  65. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
  66. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
  67. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
  68. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
  69. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
  70. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
  71. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
  72. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
  73. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
  74. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
  75. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
  76. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
  77. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
  78. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
  79. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
  80. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
  81. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
  82. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
  83. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
  84. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
  85. package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
  86. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
  87. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
  88. package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
  89. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
  90. package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
  91. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
  92. package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
  93. package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
  94. package/standard-skills/translate-native/provenance.json +2 -2
  95. package/standard-skills/translate-native/references/commercial-localization.md +96 -17
  96. package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
  97. package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
  98. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
  99. package/standard-tools/language-guard/VERSION +1 -1
  100. package/standard-tools/language-guard/blun_language_guard.py +3 -1
  101. package/standard-tools/language-guard/check_commercial_review.py +69 -9
  102. package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
  103. package/standard-tools/language-guard/provenance.json +2 -2
  104. package/standard-tools/manifest.json +1 -1
  105. package/worker-host.mjs +215 -82
  106. package/agent-spine-plugin/scripts/check-install-king.js +0 -37
@@ -3,20 +3,20 @@ import { mkdir } from "node:fs/promises";
3
3
  import { isAbsolute } from "node:path";
4
4
  import { withOwnedFileLock } from "./owned-file-lock.js";
5
5
  import {
6
- completeTimelineBinding, hasVerifiedTimelinePrivateScope, sameTimelineBinding, sessionTimelineBinding,
7
- TIMELINE_ID_RE, validTimelineBinding
6
+ completeTimelineBinding, hasVerifiedTimelinePrivateScope, sameTimelineBinding, sessionTimelineBinding,
7
+ TIMELINE_ID_RE, validTimelineBinding
8
8
  } from "./session-timeline-contract.js";
9
9
  import {
10
- ensureSessionTimelineTrust, sessionTimelineStatePaths
10
+ ensureSessionTimelineTrust, sessionTimelineStatePaths
11
11
  } from "./session-timeline-auth.js";
12
12
  import { consumeSessionTimelineInvocation, issueSessionTimelineInvocation } from "./session-timeline-invocation.js";
13
13
  import {
14
- loadPrivateSessionTimelineEnrollment, resolvePrivateSessionTimelineEnrollment
14
+ loadPrivateSessionTimelineEnrollment, resolvePrivateSessionTimelineEnrollment
15
15
  } from "./session-timeline-enrollment.js";
16
16
  import { timelineTransportEnrollmentMatches } from "./session-timeline-enrollment-transport.js";
17
17
  import { sameTimelineTransportDigest, timelineTransportDigest } from "./session-timeline-transport.js";
18
18
  import {
19
- matchesTimelineEvent, rankTimelineEvents, timelineQuery
19
+ matchesTimelineEvent, rankTimelineEvents, timelineQuery
20
20
  } from "./session-timeline-query.js";
21
21
  import { timelineContinuationCapsule, timelineSearchResult } from "./session-timeline-results.js";
22
22
  import { seekTimelineEvidence, verifyTimelineEvent } from "./session-timeline-search.js";
@@ -28,7 +28,6 @@ import { readTimelineState, saveTimelineState } from "./session-timeline-state.j
28
28
  import { sessionTimelineRootDigest } from "./session-timeline-root.js";
29
29
  import { priorTimelineHint, selectPriorTimelineSource, timelineSessionReference } from "./session-timeline-prior.js";
30
30
  import { crossProviderTimelineEnabled } from "./session-timeline-provider.js";
31
-
32
31
  export const SESSION_TIMELINE_SCHEMA = "agentspine.session-timeline/v1";
33
32
  const STATE_SCHEMA = "agentspine.session-timeline-state/v1";
34
33
  const AUTHORITY = "context-only";
@@ -41,460 +40,423 @@ const ROOM_BYTES = 1024 * 1024;
41
40
  const EVENT_OUTCOMES = new Set(["pass", "fail", "blocked", "timeout", "error", "skipped"]);
42
41
  const EVENT_LABEL_RE = /^(?:suite-(?:0|[1-9]\d{0,3})|acceptance|audit|npm-check|ci|test)$/;
43
42
  const EVENT_KEYS = new Set(["nativeMessageId", "id", "at", "offset", "bytes", "sha256", "kind", "outcome", "count", "testLabel", "nextStepSummary", "terms", "authority"]);
44
-
45
43
  function digest(value) { return createHash("sha256").update(value).digest("hex"); }
46
44
  function asDate(value) {
47
- const date = value instanceof Date ? value : new Date(value);
48
- if (!Number.isFinite(date.getTime())) throw new Error("session timeline timestamp is invalid");
49
- return date;
45
+ const date = value instanceof Date ? value : new Date(value);
46
+ if (!Number.isFinite(date.getTime())) throw new Error("session timeline timestamp is invalid");
47
+ return date;
50
48
  }
51
49
  function date(value) { return Number.isFinite(new Date(value).getTime()); }
52
-
53
50
  function empty(root) { return { schema: STATE_SCHEMA, rootDigest: sessionTimelineRootDigest(root), sources: [], authority: AUTHORITY }; }
54
51
  function hasOnlyKeys(item, allowed) { return Object.keys(item).every((key) => allowed.has(key)); }
55
52
  function validCount(item) {
56
- return item === null || Boolean(item && typeof item === "object" && Object.keys(item).length === 2
57
- && Number.isSafeInteger(item.value) && item.value >= 0 && Number.isSafeInteger(item.total)
58
- && item.total > 0 && item.value <= item.total);
53
+ return item === null || Boolean(item && typeof item === "object" && Object.keys(item).length === 2
54
+ && Number.isSafeInteger(item.value) && item.value >= 0 && Number.isSafeInteger(item.total)
55
+ && item.total > 0 && item.value <= item.total);
59
56
  }
60
57
  function validEvent(item) {
61
- const common = item && typeof item === "object" && hasOnlyKeys(item, EVENT_KEYS)
62
- && typeof item.id === "string" && TIMELINE_ID_RE.test(item.id) && date(item.at)
63
- && Number.isSafeInteger(item.offset) && item.offset >= 0 && Number.isSafeInteger(item.bytes) && item.bytes > 0
64
- && /^[a-f0-9]{64}$/.test(item.sha256 || "")
65
- && Array.isArray(item.terms) && item.terms.length <= 24 && item.terms.every((term) => /^[\p{L}\p{N}]{3,}$/u.test(term))
66
- && (item.nativeMessageId === undefined || typeof item.nativeMessageId === "string" && TIMELINE_ID_RE.test(item.nativeMessageId))
67
- && item.authority === AUTHORITY;
68
- if (!common) return false;
69
- if (item.kind === "user-message-candidate") return item.outcome === undefined
70
- && item.count === undefined && item.testLabel === undefined && item.nextStepSummary === undefined
71
- && item.terms.join(" ") === "user message";
72
- if (item.kind === "objective-result") {
73
- return EVENT_OUTCOMES.has(item.outcome) && validCount(item.count)
74
- && (item.testLabel === null || typeof item.testLabel === "string" && EVENT_LABEL_RE.test(item.testLabel))
75
- && item.nextStepSummary === undefined;
76
- }
77
- return item.kind === "explicit-next-step-correction" && item.outcome === undefined
78
- && item.count === undefined && item.testLabel === undefined
79
- && (item.nextStepSummary === undefined || typeof item.nextStepSummary === "string"
80
- && Boolean(item.nextStepSummary.trim()) && item.nextStepSummary.length <= 500);
58
+ const common = item && typeof item === "object" && hasOnlyKeys(item, EVENT_KEYS)
59
+ && typeof item.id === "string" && TIMELINE_ID_RE.test(item.id) && date(item.at)
60
+ && Number.isSafeInteger(item.offset) && item.offset >= 0 && Number.isSafeInteger(item.bytes) && item.bytes > 0
61
+ && /^[a-f0-9]{64}$/.test(item.sha256 || "")
62
+ && Array.isArray(item.terms) && item.terms.length <= 24 && item.terms.every((term) => /^[\p{L}\p{N}]{3,}$/u.test(term))
63
+ && (item.nativeMessageId === undefined || typeof item.nativeMessageId === "string" && TIMELINE_ID_RE.test(item.nativeMessageId))
64
+ && item.authority === AUTHORITY;
65
+ if (!common) return false;
66
+ if (item.kind === "user-message-candidate") return item.outcome === undefined
67
+ && item.count === undefined && item.testLabel === undefined && item.nextStepSummary === undefined
68
+ && item.terms.join(" ") === "user message";
69
+ if (item.kind === "objective-result") {
70
+ return EVENT_OUTCOMES.has(item.outcome) && validCount(item.count)
71
+ && (item.testLabel === null || typeof item.testLabel === "string" && EVENT_LABEL_RE.test(item.testLabel))
72
+ && item.nextStepSummary === undefined;
73
+ }
74
+ return item.kind === "explicit-next-step-correction" && item.outcome === undefined
75
+ && item.count === undefined && item.testLabel === undefined
76
+ && (item.nextStepSummary === undefined || typeof item.nextStepSummary === "string"
77
+ && Boolean(item.nextStepSummary.trim()) && item.nextStepSummary.length <= 500);
81
78
  }
82
79
  function validSource(item) {
83
- return item && validTimelineBinding(item.binding) && typeof item.path === "string" && isAbsolute(item.path)
84
- && item.path.length > 0 && item.path.length <= 4096 && typeof item.profileRoot === "string" && isAbsolute(item.profileRoot)
85
- && item.profileRoot.length > 0 && item.profileRoot.length <= 4096 && typeof item.projectsRoot === "string" && isAbsolute(item.projectsRoot)
86
- && item.projectsRoot.length > 0 && item.projectsRoot.length <= 4096 && item.pathDigest === digest(item.path)
87
- && typeof item.identity === "string" && item.identity.length <= 256
88
- && /^[0-9]+$/.test(item.mtimeNs || "") && /^[0-9]+$/.test(item.ctimeNs || "")
89
- && Number.isInteger(item.indexedBytes) && item.indexedBytes >= 0 && Number.isInteger(item.size) && item.size >= 0
90
- && item.indexedBytes <= item.size && Array.isArray(item.events) && item.events.length <= MAX_EVENTS
91
- && item.events.every((event) => validEvent(event) && event.bytes <= MAX_LINE_BYTES && event.offset + event.bytes <= item.size)
92
- && (item.lessonDigest === null || /^[a-f0-9]{64}$/.test(item.lessonDigest || ""))
93
- && date(item.updatedAt) && item.authority === AUTHORITY;
80
+ return item && validTimelineBinding(item.binding) && typeof item.path === "string" && isAbsolute(item.path)
81
+ && item.path.length > 0 && item.path.length <= 4096 && typeof item.profileRoot === "string" && isAbsolute(item.profileRoot)
82
+ && item.profileRoot.length > 0 && item.profileRoot.length <= 4096 && typeof item.projectsRoot === "string" && isAbsolute(item.projectsRoot)
83
+ && item.projectsRoot.length > 0 && item.projectsRoot.length <= 4096 && item.pathDigest === digest(item.path)
84
+ && typeof item.identity === "string" && item.identity.length <= 256
85
+ && /^[0-9]+$/.test(item.mtimeNs || "") && /^[0-9]+$/.test(item.ctimeNs || "")
86
+ && Number.isInteger(item.indexedBytes) && item.indexedBytes >= 0 && Number.isInteger(item.size) && item.size >= 0
87
+ && item.indexedBytes <= item.size && Array.isArray(item.events) && item.events.length <= MAX_EVENTS
88
+ && item.events.every((event) => validEvent(event) && event.bytes <= MAX_LINE_BYTES && event.offset + event.bytes <= item.size)
89
+ && (item.lessonDigest === null || /^[a-f0-9]{64}$/.test(item.lessonDigest || ""))
90
+ && date(item.updatedAt) && item.authority === AUTHORITY;
94
91
  }
95
92
  function validate(value, root) {
96
- if (!value || value.schema !== STATE_SCHEMA || value.rootDigest !== sessionTimelineRootDigest(root) || value.authority !== AUTHORITY
97
- || !Array.isArray(value.sources) || value.sources.length > MAX_SOURCES || value.sources.some((item) => !validSource(item))) {
98
- throw new Error("session timeline state is invalid");
99
- }
100
- return value;
93
+ if (!value || value.schema !== STATE_SCHEMA || value.rootDigest !== sessionTimelineRootDigest(root) || value.authority !== AUTHORITY
94
+ || !Array.isArray(value.sources) || value.sources.length > MAX_SOURCES || value.sources.some((item) => !validSource(item))) {
95
+ throw new Error("session timeline state is invalid");
96
+ }
97
+ return value;
101
98
  }
102
-
103
99
  function paths(root) { return sessionTimelineStatePaths(root); }
104
100
  function readState(path, root, assertStable) {
105
- return readTimelineState({ path, root, maximumBytes: MAX_STATE_BYTES, empty, validate, assertStable });
101
+ return readTimelineState({ path, root, maximumBytes: MAX_STATE_BYTES, empty, validate, assertStable });
106
102
  }
107
103
  function saveState(state, path, assertOwned, root, assertStable) {
108
- return saveTimelineState({ state, path, root, maximumBytes: MAX_STATE_BYTES, assertOwned, assertStable });
104
+ return saveTimelineState({ state, path, root, maximumBytes: MAX_STATE_BYTES, assertOwned, assertStable });
109
105
  }
110
-
111
106
  function status(value, extra = {}) { return { schema: SESSION_TIMELINE_SCHEMA, ...value, ...extra, authority: AUTHORITY }; }
112
107
  function hasExactPrivateTimelineScope(scope) { return scope?.groupId === null; }
113
108
  function sourceFor(state, scope) { return state.sources.find((item) => sameTimelineBinding(item.binding, scope)) || null; }
114
109
  function sourceMetadata(source) {
115
- const sourceDigest = digest(`${source.pathDigest}\0${source.identity}\0${source.size}\0${source.mtimeNs}\0${source.ctimeNs}`);
116
- return { sourceDigest, indexedBytes: source.indexedBytes, size: source.size, events: source.events.length,
117
- rooms: Math.ceil(source.size / ROOM_BYTES), continuation: timelineContinuationCapsule({ source, sourceDigest, roomBytes: ROOM_BYTES, authority: AUTHORITY }) };
110
+ const sourceDigest = digest(`${source.pathDigest}\0${source.identity}\0${source.size}\0${source.mtimeNs}\0${source.ctimeNs}`);
111
+ return { sourceDigest, indexedBytes: source.indexedBytes, size: source.size, events: source.events.length,
112
+ rooms: Math.ceil(source.size / ROOM_BYTES), continuation: timelineContinuationCapsule({ source, sourceDigest, roomBytes: ROOM_BYTES, authority: AUTHORITY }) };
118
113
  }
119
114
  function sameSourceSnapshot(left, right) {
120
- return Boolean(left && right) && ["path", "profileRoot", "projectsRoot", "pathDigest", "identity", "size", "mtimeNs", "ctimeNs"]
121
- .every((key) => left[key] === right[key]);
115
+ return Boolean(left && right) && ["path", "profileRoot", "projectsRoot", "pathDigest", "identity", "size", "mtimeNs", "ctimeNs"]
116
+ .every((key) => left[key] === right[key]);
122
117
  }
123
118
  async function confirmedSourceEnrollment({ root, scoped, source, hostHome }) {
124
- const enrollment = await resolvePrivateSessionTimelineEnrollment({ root, host: scoped.host, sessionId: scoped.sessionId,
125
- transcriptPath: source.path, hostHome });
126
- if (enrollment.status !== "enrolled" || !sameTimelineBinding(enrollment.binding, scoped)
127
- || !sameSourceSnapshot(enrollment.source, source)) return false;
128
- return true;
119
+ const enrollment = await resolvePrivateSessionTimelineEnrollment({ root, host: scoped.host, sessionId: scoped.sessionId,
120
+ transcriptPath: source.path, hostHome });
121
+ if (enrollment.status !== "enrolled" || !sameTimelineBinding(enrollment.binding, scoped)
122
+ || !sameSourceSnapshot(enrollment.source, source)) return false;
123
+ return true;
129
124
  }
130
-
131
125
  async function bootstrapEnrollment(root, enrollmentDigest, environment) {
132
- const loaded = await loadPrivateSessionTimelineEnrollment({ root, enrollmentDigest });
133
- if (loaded.status !== "loaded") return null;
134
- const transportDigest = timelineTransportDigest({
135
- root: loaded.rootPath, binding: loaded.record.binding, environment
136
- });
137
- return transportDigest && sameTimelineTransportDigest(loaded.record.transportDigest, transportDigest) ? loaded : null;
138
- }
139
-
126
+ const loaded = await loadPrivateSessionTimelineEnrollment({ root, enrollmentDigest });
127
+ if (loaded.status !== "loaded") return null;
128
+ const transportDigest = timelineTransportDigest({
129
+ root: loaded.rootPath, binding: loaded.record.binding, environment
130
+ });
131
+ return transportDigest && sameTimelineTransportDigest(loaded.record.transportDigest, transportDigest) ? loaded : null;
132
+ }
140
133
  function enrolledSource(record, previous, now) {
141
- const source = record.source;
142
- const unchanged = previous && sameSourceSnapshot(previous, source);
143
- return {
144
- binding: record.binding, path: source.path, profileRoot: source.profileRoot, projectsRoot: source.projectsRoot,
145
- pathDigest: source.pathDigest, identity: source.identity, size: source.size, mtimeNs: source.mtimeNs,
146
- ctimeNs: source.ctimeNs, lessonDigest: unchanged ? previous.lessonDigest : null,
147
- indexedBytes: unchanged ? previous.indexedBytes : 0, events: unchanged ? previous.events : [],
148
- updatedAt: asDate(now).toISOString(), authority: AUTHORITY
149
- };
150
- }
151
-
152
- // Bootstrap has no caller-provided source location. It copies only the source
153
- // metadata sealed in the just-validated enrollment into the private sidecar;
154
- // the MCP index remains the first and only path that opens transcript bytes.
134
+ const source = record.source;
135
+ const unchanged = previous && sameSourceSnapshot(previous, source);
136
+ return {
137
+ binding: record.binding, path: source.path, profileRoot: source.profileRoot, projectsRoot: source.projectsRoot,
138
+ pathDigest: source.pathDigest, identity: source.identity, size: source.size, mtimeNs: source.mtimeNs,
139
+ ctimeNs: source.ctimeNs, lessonDigest: unchanged ? previous.lessonDigest : null,
140
+ indexedBytes: unchanged ? previous.indexedBytes : 0, events: unchanged ? previous.events : [],
141
+ updatedAt: asDate(now).toISOString(), authority: AUTHORITY
142
+ };
143
+ }
155
144
  export async function bootstrapSessionTimelineEnrollment({
156
- root, enrollmentDigest, environment = process.env, now = new Date()
145
+ root, enrollmentDigest, environment = process.env, now = new Date()
157
146
  }) {
158
- const loaded = await bootstrapEnrollment(root, enrollmentDigest, environment);
159
- if (!loaded) return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
160
- const { rootPath, record } = loaded;
161
- try { await ensureSessionTimelineTrust({ create: true }); }
162
- catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
163
- try {
164
- const names = await paths(rootPath);
165
- await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
166
- const state = await readState(names.path, rootPath, names.assertStable);
167
- const previous = sourceFor(state, record.binding);
168
- if (previous && sameSourceSnapshot(previous, record.source)) return;
169
- state.sources = state.sources.filter((item) => !sameTimelineBinding(item.binding, record.binding));
170
- state.sources.unshift(enrolledSource(record, previous, now));
171
- state.sources = state.sources.slice(0, MAX_SOURCES);
172
- await saveState(state, names.path, assertOwned, rootPath, names.assertStable);
173
- }, { assertPath: names.assertStable });
174
- } catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
175
- return status({ status: "registered", bootstrap: "signed-enrollment-metadata" });
176
- }
177
-
178
- // Hooks use this deliberately source-blind hint after compaction. It proves
179
- // only that a matching signed enrollment and sidecar still exist; the next
180
- // bound MCP index/search revalidates the immutable source before any read.
147
+ const loaded = await bootstrapEnrollment(root, enrollmentDigest, environment);
148
+ if (!loaded) return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
149
+ const { rootPath, record } = loaded;
150
+ try { await ensureSessionTimelineTrust({ create: true }); }
151
+ catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
152
+ try {
153
+ const names = await paths(rootPath);
154
+ await withOwnedFileLock(names.lock, async ({ assertOwned }) => {
155
+ const state = await readState(names.path, rootPath, names.assertStable);
156
+ const previous = sourceFor(state, record.binding);
157
+ if (previous && sameSourceSnapshot(previous, record.source)) return;
158
+ state.sources = state.sources.filter((item) => !sameTimelineBinding(item.binding, record.binding));
159
+ state.sources.unshift(enrolledSource(record, previous, now));
160
+ state.sources = state.sources.slice(0, MAX_SOURCES);
161
+ await saveState(state, names.path, assertOwned, rootPath, names.assertStable);
162
+ }, { assertPath: names.assertStable });
163
+ } catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
164
+ return status({ status: "registered", bootstrap: "signed-enrollment-metadata" });
165
+ }
181
166
  export async function sessionTimelineLifecycleHint({
182
- root, host, sessionId, scope, environment = process.env
167
+ root, host, sessionId, scope, environment = process.env
183
168
  }) {
184
- const scoped = sessionTimelineBinding({ host, sessionId, scope });
185
- if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
186
- // Lifecycle input cannot assert private visibility. Its exact signed
187
- // enrollment below is the authority for this source-blind status hint.
188
- if (!completeTimelineBinding(scoped)) {
189
- return status({ status: "unavailable", reason: "timeline-scope-unverified" });
190
- }
191
- const loaded = await loadPrivateSessionTimelineEnrollment({ root, host, sessionId, scope });
192
- if (loaded.status !== "loaded") return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
193
- const transportDigest = timelineTransportDigest({ root: loaded.rootPath, binding: loaded.record.binding, environment });
194
- if (!transportDigest || !sameTimelineTransportDigest(loaded.record.transportDigest, transportDigest)) {
195
- return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
196
- }
197
- try {
198
- await ensureSessionTimelineTrust();
199
- const names = await paths(loaded.rootPath);
200
- const state = await readState(names.path, loaded.rootPath, names.assertStable);
201
- const source = sourceFor(state, scoped);
202
- if (!source) return status({ status: "unavailable", reason: "timeline-not-registered" });
203
- return status({ status: source.indexedBytes >= source.size ? "indexed" : "partial", ...sourceMetadata(source),
204
- priorSessions: priorTimelineHint(state, scoped, (item) => sourceMetadata(item).sourceDigest,
205
- { includePriorProviders: crossProviderTimelineEnabled(environment) }),
206
- freshness: "source-not-read", instruction: "Use session_timeline_index before search when this snapshot is partial." });
207
- } catch {
208
- return status({ status: "unavailable", reason: "timeline-state-unavailable" });
209
- }
210
- }
211
-
212
- // Source registration used to accept a raw caller path. That would turn a
213
- // direct API call into a second transcript reader, so the only supported
214
- // setup is now receipt-backed enrollment followed by signed metadata bootstrap.
215
- // Keep this explicit no-data result for callers upgrading from the legacy API.
169
+ const scoped = sessionTimelineBinding({ host, sessionId, scope });
170
+ if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
171
+ if (!completeTimelineBinding(scoped)) {
172
+ return status({ status: "unavailable", reason: "timeline-scope-unverified" });
173
+ }
174
+ const loaded = await loadPrivateSessionTimelineEnrollment({ root, host, sessionId, scope });
175
+ if (loaded.status !== "loaded") return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
176
+ const transportDigest = timelineTransportDigest({ root: loaded.rootPath, binding: loaded.record.binding, environment });
177
+ if (!transportDigest || !sameTimelineTransportDigest(loaded.record.transportDigest, transportDigest)) {
178
+ return status({ status: "unavailable", reason: "private-enrollment-unavailable" });
179
+ }
180
+ try {
181
+ await ensureSessionTimelineTrust();
182
+ const names = await paths(loaded.rootPath);
183
+ const state = await readState(names.path, loaded.rootPath, names.assertStable);
184
+ const source = sourceFor(state, scoped);
185
+ if (!source) return status({ status: "unavailable", reason: "timeline-not-registered" });
186
+ return status({ status: source.indexedBytes >= source.size ? "indexed" : "partial", ...sourceMetadata(source),
187
+ priorSessions: priorTimelineHint(state, scoped, (item) => sourceMetadata(item).sourceDigest,
188
+ { includePriorProviders: crossProviderTimelineEnabled(environment) }),
189
+ freshness: "source-not-read", instruction: "Use session_timeline_index before search when this snapshot is partial." });
190
+ } catch {
191
+ return status({ status: "unavailable", reason: "timeline-state-unavailable" });
192
+ }
193
+ }
216
194
  export async function registerSessionTimelineSource() {
217
- return status({ status: "unavailable", reason: "timeline-enrollment-bootstrap-required" });
195
+ return status({ status: "unavailable", reason: "timeline-enrollment-bootstrap-required" });
218
196
  }
219
-
220
197
  function mergeEvents(existing, additions) {
221
- const byId = new Map(existing.map((item) => [item.id, item]));
222
- for (const item of additions) byId.set(item.id, item);
223
- const ordered = [...byId.values()].sort((left, right) => left.at.localeCompare(right.at) || left.offset - right.offset);
224
- const evidence = ordered.filter((item) => item.kind !== "user-message-candidate").slice(-MAX_EVENTS);
225
- const available = Math.min(256, MAX_EVENTS - evidence.length);
226
- const feedback = ordered.filter((item) => item.kind === "user-message-candidate")
227
- .slice(available ? -available : ordered.length);
228
- return [...evidence, ...feedback].sort((left, right) => left.at.localeCompare(right.at) || left.offset - right.offset);
198
+ const byId = new Map(existing.map((item) => [item.id, item]));
199
+ for (const item of additions) byId.set(item.id, item);
200
+ const ordered = [...byId.values()].sort((left, right) => left.at.localeCompare(right.at) || left.offset - right.offset);
201
+ const evidence = ordered.filter((item) => item.kind !== "user-message-candidate").slice(-MAX_EVENTS);
202
+ const available = Math.min(256, MAX_EVENTS - evidence.length);
203
+ const feedback = ordered.filter((item) => item.kind === "user-message-candidate")
204
+ .slice(available ? -available : ordered.length);
205
+ return [...evidence, ...feedback].sort((left, right) => left.at.localeCompare(right.at) || left.offset - right.offset);
229
206
  }
230
207
  function parsedEvents(buffer, start, dropFirst = false, host = "claude", includeUserMessages = false) {
231
- const result = [];
232
- let index = 0;
233
- while (index < buffer.byteLength) {
234
- const newline = buffer.indexOf(0x0a, index);
235
- const end = newline < 0 ? buffer.byteLength : newline + 1;
236
- const line = buffer.subarray(index, end);
237
- if (!(dropFirst && index === 0) && line.byteLength) {
238
- const event = eventFromTimelineLine(line.toString("utf8"), start + index, AUTHORITY, host);
239
- if (event && (includeUserMessages || event.kind !== "user-message-candidate")) {
240
- if (event.kind === "explicit-next-step-correction") delete event.nextStepSummary;
241
- delete event.sourceText;
242
- result.push(event);
243
- }
244
- }
245
- index = end;
246
- }
247
- return result;
248
- }
249
-
208
+ const result = [];
209
+ let index = 0;
210
+ while (index < buffer.byteLength) {
211
+ const newline = buffer.indexOf(0x0a, index);
212
+ const end = newline < 0 ? buffer.byteLength : newline + 1;
213
+ const line = buffer.subarray(index, end);
214
+ if (!(dropFirst && index === 0) && line.byteLength) {
215
+ const event = eventFromTimelineLine(line.toString("utf8"), start + index, AUTHORITY, host);
216
+ if (event && (includeUserMessages || event.kind !== "user-message-candidate")) {
217
+ if (event.kind === "explicit-next-step-correction") delete event.nextStepSummary;
218
+ delete event.sourceText;
219
+ result.push(event);
220
+ }
221
+ }
222
+ index = end;
223
+ }
224
+ return result;
225
+ }
250
226
  async function indexRange(source, start, maximum, hostHome = null) {
251
- const opened = await validatedHandle(source, hostHome);
252
- if (opened.status !== "open") return opened;
253
- try {
254
- if (start >= opened.size) return { status: "indexed", next: opened.size, events: [], size: opened.size };
255
- const bytes = await readRange(opened.handle, start, Math.min(maximum, opened.size - start));
256
- const last = bytes.lastIndexOf(0x0a);
257
- const completeAtEnd = start + bytes.byteLength === opened.size;
258
- if (last < 0 && !completeAtEnd) return { status: "partial-line", events: [], size: opened.size };
259
- const selected = last < 0 || completeAtEnd ? bytes : bytes.subarray(0, last + 1);
260
- const before = start > 0 ? await readRange(opened.handle, start - 1, 1) : Buffer.alloc(0);
261
- if (!await unchangedHandle(opened.handle, source, hostHome)) return { status: "unavailable", reason: "transcript-changed" };
262
- return { status: "indexed", next: start + selected.byteLength,
263
- events: parsedEvents(selected, start, start > 0 && before[0] !== 0x0a, source.binding.host,
264
- Boolean(source.binding.portalRef && source.binding.threadRef)), size: opened.size };
265
- } finally { await opened.handle.close(); }
266
- }
267
-
227
+ const opened = await validatedHandle(source, hostHome);
228
+ if (opened.status !== "open") return opened;
229
+ try {
230
+ if (start >= opened.size) return { status: "indexed", next: opened.size, events: [], size: opened.size };
231
+ const bytes = await readRange(opened.handle, start, Math.min(maximum, opened.size - start));
232
+ const last = bytes.lastIndexOf(0x0a);
233
+ const completeAtEnd = start + bytes.byteLength === opened.size;
234
+ if (last < 0 && !completeAtEnd) return { status: "partial-line", events: [], size: opened.size };
235
+ const selected = last < 0 || completeAtEnd ? bytes : bytes.subarray(0, last + 1);
236
+ const before = start > 0 ? await readRange(opened.handle, start - 1, 1) : Buffer.alloc(0);
237
+ if (!await unchangedHandle(opened.handle, source, hostHome)) return { status: "unavailable", reason: "transcript-changed" };
238
+ return { status: "indexed", next: start + selected.byteLength,
239
+ events: parsedEvents(selected, start, start > 0 && before[0] !== 0x0a, source.binding.host,
240
+ Boolean(source.binding.portalRef && source.binding.threadRef)), size: opened.size };
241
+ } finally { await opened.handle.close(); }
242
+ }
268
243
  async function mutateSource(root, scoped, task) {
269
- try { await ensureSessionTimelineTrust(); }
270
- catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
271
- let names;
272
- try { names = await paths(root); }
273
- catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
274
- return withOwnedFileLock(names.lock, async ({ assertOwned }) => {
275
- let state;
276
- try { state = await readState(names.path, root, names.assertStable); }
277
- catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
278
- const source = sourceFor(state, scoped);
279
- if (!source) return status({ status: "unavailable", reason: "timeline-not-registered" });
280
- const result = await task(source);
281
- if (result.status === "indexed") {
282
- source.size = result.size;
283
- if (!result.preserveCursor) source.indexedBytes = Math.max(source.indexedBytes, result.next);
284
- source.events = mergeEvents(source.events, result.events); source.updatedAt = new Date().toISOString();
285
- await saveState(state, names.path, assertOwned, root, names.assertStable);
286
- }
287
- return result.status === "indexed" ? status({ status: source.indexedBytes >= source.size ? "indexed"
288
- : result.preserveCursor ? "fresh-tail" : "partial", ...sourceMetadata(source), added: result.events.length })
289
- : status(result);
290
- }, { assertPath: names.assertStable }).catch(() =>
291
- status({ status: "unavailable", reason: "timeline-state-unavailable" }));
292
- }
293
-
244
+ try { await ensureSessionTimelineTrust(); }
245
+ catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
246
+ let names;
247
+ try { names = await paths(root); }
248
+ catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
249
+ return withOwnedFileLock(names.lock, async ({ assertOwned }) => {
250
+ let state;
251
+ try { state = await readState(names.path, root, names.assertStable); }
252
+ catch { return status({ status: "unavailable", reason: "timeline-state-unavailable" }); }
253
+ const source = sourceFor(state, scoped);
254
+ if (!source) return status({ status: "unavailable", reason: "timeline-not-registered" });
255
+ const result = await task(source);
256
+ if (result.status === "indexed") {
257
+ source.size = result.size;
258
+ if (!result.preserveCursor) source.indexedBytes = Math.max(source.indexedBytes, result.next);
259
+ source.events = mergeEvents(source.events, result.events); source.updatedAt = new Date().toISOString();
260
+ await saveState(state, names.path, assertOwned, root, names.assertStable);
261
+ }
262
+ return result.status === "indexed" ? status({ status: source.indexedBytes >= source.size ? "indexed"
263
+ : result.preserveCursor ? "fresh-tail" : "partial", ...sourceMetadata(source), added: result.events.length })
264
+ : status(result);
265
+ }, { assertPath: names.assertStable }).catch(() =>
266
+ status({ status: "unavailable", reason: "timeline-state-unavailable" }));
267
+ }
294
268
  export async function indexSessionTimeline({
295
- root, host, sessionId, scope, maxBytes = 4 * 1024 * 1024,
296
- invocationRequest = null, transportDigest = null, enrollmentDigest = null, hostHome = null
269
+ root, host, sessionId, scope, maxBytes = 4 * 1024 * 1024,
270
+ invocationRequest = null, transportDigest = null, enrollmentDigest = null, hostHome = null
297
271
  }) {
298
- const scoped = sessionTimelineBinding({ host, sessionId, scope });
299
- if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
300
- if (!hasVerifiedTimelinePrivateScope(scope)) return status({ status: "unavailable", reason: "timeline-scope-unverified" });
301
- if (!completeTimelineBinding(scoped)) return status({ status: "unavailable", reason: "missing-session-scope" });
302
- if (!Number.isInteger(maxBytes) || maxBytes < 64 * 1024 || maxBytes > MAX_INDEX_BYTES) throw new Error("timeline index byte budget is invalid");
303
- return mutateSource(root, scoped, async (source) => {
304
- if (!await confirmedSourceEnrollment({ root, scoped, source, hostHome })) {
305
- return { status: "unavailable", reason: "private-enrollment-unavailable" };
306
- }
307
- const sourceDigest = sourceMetadata(source).sourceDigest;
308
- if (!invocationRequest || !await timelineTransportEnrollmentMatches({ root, binding: scoped, enrollmentDigest, transportDigest, hostHome })
309
- || !await consumeSessionTimelineInvocation({ root, tool: "index", binding: scoped, sourceDigest,
310
- request: invocationRequest, transportDigest })) {
311
- return { status: "unavailable", reason: "timeline-invocation-unavailable" };
312
- }
313
- return indexRange(source, source.indexedBytes, maxBytes, hostHome);
314
- });
315
- }
316
-
317
- // Tail refresh is intentionally retired: only a bound MCP index invocation
318
- // may open an enrolled transcript, and it must consume its one-use permit.
272
+ const scoped = sessionTimelineBinding({ host, sessionId, scope });
273
+ if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
274
+ if (!hasVerifiedTimelinePrivateScope(scope)) return status({ status: "unavailable", reason: "timeline-scope-unverified" });
275
+ if (!completeTimelineBinding(scoped)) return status({ status: "unavailable", reason: "missing-session-scope" });
276
+ if (!Number.isInteger(maxBytes) || maxBytes < 64 * 1024 || maxBytes > MAX_INDEX_BYTES) throw new Error("timeline index byte budget is invalid");
277
+ return mutateSource(root, scoped, async (source) => {
278
+ if (!await confirmedSourceEnrollment({ root, scoped, source, hostHome })) {
279
+ return { status: "unavailable", reason: "private-enrollment-unavailable" };
280
+ }
281
+ const sourceDigest = sourceMetadata(source).sourceDigest;
282
+ if (!invocationRequest || !await timelineTransportEnrollmentMatches({ root, binding: scoped, enrollmentDigest, transportDigest, hostHome })
283
+ || !await consumeSessionTimelineInvocation({ root, tool: "index", binding: scoped, sourceDigest,
284
+ request: invocationRequest, transportDigest })) {
285
+ return { status: "unavailable", reason: "timeline-invocation-unavailable" };
286
+ }
287
+ return indexRange(source, source.indexedBytes, maxBytes, hostHome);
288
+ });
289
+ }
319
290
  export async function refreshSessionTimelineTail() {
320
- return status({ status: "unavailable", reason: "timeline-mcp-index-required" });
291
+ return status({ status: "unavailable", reason: "timeline-mcp-index-required" });
321
292
  }
322
-
323
293
  export async function sessionTimelineStatus({ root, host, sessionId, scope, environment = process.env }) {
324
- if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
325
- if (!hasVerifiedTimelinePrivateScope(scope)) return status({ status: "unavailable", reason: "timeline-scope-unverified" });
326
- // Status is intentionally a sidecar-only hint. Immutable source validation
327
- // is kept at receipt consumption and PreTool, while MCP index/search alone
328
- // perform bounded evidence reads.
329
- return sessionTimelineLifecycleHint({ root, host, sessionId, scope, environment });
330
- }
331
-
294
+ if (!hasExactPrivateTimelineScope(scope)) return status({ status: "group-suppressed" });
295
+ if (!hasVerifiedTimelinePrivateScope(scope)) return status({ status: "unavailable", reason: "timeline-scope-unverified" });
296
+ return sessionTimelineLifecycleHint({ root, host, sessionId, scope, environment });
297
+ }
332
298
  export async function authorizeSessionTimelineInvocation({
333
- root, host, sessionId, scope, hostHome, tool, request, toolUseId, transportDigest, enrollmentDigest,
334
- environment = process.env
299
+ root, host, sessionId, scope, hostHome, tool, request, toolUseId, transportDigest, enrollmentDigest,
300
+ environment = process.env
335
301
  }) {
336
- const scoped = sessionTimelineBinding({ host, sessionId, scope });
337
- if (!hasExactPrivateTimelineScope(scope) || !completeTimelineBinding(scoped) || !/^(index|search|capture)$/.test(tool || "")) return null;
338
- if (!hasVerifiedTimelinePrivateScope(scope)) return null;
339
- try {
340
- await ensureSessionTimelineTrust();
341
- const names = await paths(root);
342
- const state = await readState(names.path, root, names.assertStable);
343
- const currentSource = sourceFor(state, scoped);
344
- if (!currentSource || !await pathMatchesSource(currentSource, hostHome)
345
- || !await confirmedSourceEnrollment({ root, scoped, source: currentSource, hostHome })) return null;
346
- const includePriorProviders = request?.includePriorProviders === true;
347
- if (includePriorProviders && (request?.includePriorSessions !== true || !crossProviderTimelineEnabled(environment))) return null;
348
- const query = tool !== "index" ? timelineQuery({ at: request?.at, query: request?.query }) : null;
349
- const source = request?.includePriorSessions === true
350
- ? selectPriorTimelineSource(state, scoped, { ...query, windowMs: request.windowSeconds * 1000,
351
- includePriorProviders }) : currentSource;
352
- const sourceHome = source?.binding.host === scoped.host ? hostHome : source?.profileRoot;
353
- if (!source || !await pathMatchesSource(source, sourceHome)
354
- || !await confirmedSourceEnrollment({ root, scoped: source.binding, source, hostHome: sourceHome })) return null;
355
- const sourceDigest = sourceMetadata(source).sourceDigest;
356
- if (!await timelineTransportEnrollmentMatches({ root, binding: scoped, enrollmentDigest, transportDigest, hostHome })) return null;
357
- await issueSessionTimelineInvocation({
358
- root, tool, binding: scoped, sourceDigest, request, toolUseId, transportDigest
359
- });
360
- return { sourceDigest, authority: AUTHORITY };
361
- } catch { return null; }
362
- }
363
-
364
- // Reopen only exact, already-captured sources needed by one interpretation.
365
- // The outer capture invocation remains the sole public gate.
302
+ const scoped = sessionTimelineBinding({ host, sessionId, scope });
303
+ if (!hasExactPrivateTimelineScope(scope) || !completeTimelineBinding(scoped) || !/^(index|search|capture)$/.test(tool || "")) return null;
304
+ if (!hasVerifiedTimelinePrivateScope(scope)) return null;
305
+ try {
306
+ await ensureSessionTimelineTrust();
307
+ const names = await paths(root);
308
+ const state = await readState(names.path, root, names.assertStable);
309
+ const currentSource = sourceFor(state, scoped);
310
+ if (!currentSource || !await pathMatchesSource(currentSource, hostHome)
311
+ || !await confirmedSourceEnrollment({ root, scoped, source: currentSource, hostHome })) return null;
312
+ const includePriorProviders = request?.includePriorProviders === true;
313
+ if (includePriorProviders && (request?.includePriorSessions !== true || !crossProviderTimelineEnabled(environment))) return null;
314
+ const query = tool !== "index" ? timelineQuery({ at: request?.at, query: request?.query }) : null;
315
+ const source = request?.includePriorSessions === true
316
+ ? selectPriorTimelineSource(state, scoped, { ...query, windowMs: request.windowSeconds * 1000,
317
+ includePriorProviders, sessionRef: request?.ref }) : currentSource;
318
+ const sourceHome = source?.binding.host === scoped.host ? hostHome : source?.profileRoot;
319
+ if (!source || !await pathMatchesSource(source, sourceHome)
320
+ || !await confirmedSourceEnrollment({ root, scoped: source.binding, source, hostHome: sourceHome })) return null;
321
+ const sourceDigest = sourceMetadata(source).sourceDigest;
322
+ if (!await timelineTransportEnrollmentMatches({ root, binding: scoped, enrollmentDigest, transportDigest, hostHome })) return null;
323
+ await issueSessionTimelineInvocation({
324
+ root, tool, binding: scoped, sourceDigest, request, toolUseId, transportDigest
325
+ });
326
+ return { sourceDigest, authority: AUTHORITY };
327
+ } catch { return null; }
328
+ }
366
329
  export async function reverifyTimelineFeedbackSources({
367
- root, host, sessionId, scope, references, includePriorProviders = false,
368
- environment = process.env, hostHome = null
330
+ root, host, sessionId, scope, references, includePriorProviders = false,
331
+ environment = process.env, hostHome = null
369
332
  }) {
370
- const scoped = sessionTimelineBinding({ host, sessionId, scope });
371
- if (!completeTimelineBinding(scoped) || !hasVerifiedTimelinePrivateScope(scope)
372
- || !Array.isArray(references) || !references.length || references.length > 3
373
- || includePriorProviders && !crossProviderTimelineEnabled(environment)) return null;
374
- try {
375
- await ensureSessionTimelineTrust();
376
- const names = await paths(root);
377
- const state = await readState(names.path, root, names.assertStable);
378
- const fields = ["entityId", "userId", "tenantId", "projectId", "taskId", "groupId", "portalRef", "threadRef"];
379
- const eligible = state.sources.filter((source) => fields.every((key) =>
380
- (source.binding[key] ?? null) === (scoped[key] ?? null))
381
- && (includePriorProviders || source.binding.host === scoped.host));
382
- const verified = [];
383
- for (const reference of references) {
384
- const source = eligible.find((item) => item.binding.host === reference.sourceProvider
385
- && timelineSessionReference(item.binding) === reference.sessionRef
386
- && sourceMetadata(item).sourceDigest === reference.sourceDigest);
387
- const sourceHome = source?.binding.host === scoped.host ? hostHome : source?.profileRoot;
388
- const indexed = source?.events.find((item) => item.id === reference.eventId
389
- && item.sha256 === reference.messageDigest && item.at === reference.observedAt);
390
- if (!source || !indexed || reference.messageRef !== reference.eventId
391
- || !await confirmedSourceEnrollment({ root, scoped: source.binding, source, hostHome: sourceHome })) return null;
392
- const opened = await validatedHandle(source, sourceHome);
393
- if (opened.status !== "open") return null;
394
- try {
395
- const current = await verifyTimelineEvent({ handle: opened.handle, event: indexed, readRange, digest,
396
- eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host) });
397
- if (!current || current.kind !== "user-message-candidate"
398
- || !await unchangedHandle(opened.handle, source, sourceHome)) return null;
399
- verified.push({ ...current, sourceDigest: reference.sourceDigest,
400
- sourceProvider: reference.sourceProvider, sessionRef: reference.sessionRef });
401
- } finally { await opened.handle.close(); }
402
- }
403
- return verified;
404
- } catch { return null; }
405
- }
406
-
333
+ const scoped = sessionTimelineBinding({ host, sessionId, scope });
334
+ if (!completeTimelineBinding(scoped) || !hasVerifiedTimelinePrivateScope(scope)
335
+ || !Array.isArray(references) || !references.length || references.length > 3
336
+ || includePriorProviders && !crossProviderTimelineEnabled(environment)) return null;
337
+ try {
338
+ await ensureSessionTimelineTrust();
339
+ const names = await paths(root);
340
+ const state = await readState(names.path, root, names.assertStable);
341
+ const fields = ["entityId", "userId", "tenantId", "projectId", "taskId", "groupId", "portalRef", "threadRef"];
342
+ const eligible = state.sources.filter((source) => fields.every((key) =>
343
+ (source.binding[key] ?? null) === (scoped[key] ?? null))
344
+ && (includePriorProviders || source.binding.host === scoped.host));
345
+ const verified = [];
346
+ for (const reference of references) {
347
+ const source = eligible.find((item) => item.binding.host === reference.sourceProvider
348
+ && timelineSessionReference(item.binding) === reference.sessionRef
349
+ && sourceMetadata(item).sourceDigest === reference.sourceDigest);
350
+ const sourceHome = source?.binding.host === scoped.host ? hostHome : source?.profileRoot;
351
+ const indexed = source?.events.find((item) => item.id === reference.eventId
352
+ && item.sha256 === reference.messageDigest && item.at === reference.observedAt);
353
+ if (!source || !indexed || reference.messageRef !== reference.eventId
354
+ || !await confirmedSourceEnrollment({ root, scoped: source.binding, source, hostHome: sourceHome })) return null;
355
+ const opened = await validatedHandle(source, sourceHome);
356
+ if (opened.status !== "open") return null;
357
+ try {
358
+ const current = await verifyTimelineEvent({ handle: opened.handle, event: indexed, readRange, digest,
359
+ eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host) });
360
+ if (!current || current.kind !== "user-message-candidate"
361
+ || !await unchangedHandle(opened.handle, source, sourceHome)) return null;
362
+ verified.push({ ...current, sourceDigest: reference.sourceDigest,
363
+ sourceProvider: reference.sourceProvider, sessionRef: reference.sessionRef });
364
+ } finally { await opened.handle.close(); }
365
+ }
366
+ return verified;
367
+ } catch { return null; }
368
+ }
407
369
  function searchResult(source, target, wanted, mode, events, extra = {}, includeMessageDigest = false) {
408
- const index = sourceMetadata(source);
409
- return timelineSearchResult({ sourceDigest: index.sourceDigest, sessionRef: timelineSessionReference(source.binding),
410
- sourceProvider: source.binding.host, binding: source.binding, target, wanted, mode, events, index,
411
- roomBytes: ROOM_BYTES, authority: AUTHORITY, extra, includeMessageDigest });
370
+ const index = sourceMetadata(source);
371
+ return timelineSearchResult({ sourceDigest: index.sourceDigest, sessionRef: timelineSessionReference(source.binding),
372
+ sourceProvider: source.binding.host, binding: source.binding, target, wanted, mode, events, index,
373
+ roomBytes: ROOM_BYTES, authority: AUTHORITY, extra, includeMessageDigest });
412
374
  }
413
-
414
375
  export async function searchSessionTimeline({
415
- root, host, sessionId, scope, at, query, windowSeconds = undefined,
416
- includePriorSessions = false, includePriorProviders = false, environment = process.env,
417
- invocationRequest = null, invocationTool = "search", transportDigest = null, enrollmentDigest = null, hostHome = null
376
+ root, host, sessionId, scope, at, query, windowSeconds = undefined,
377
+ includePriorSessions = false, includePriorProviders = false, environment = process.env,
378
+ invocationRequest = null, invocationTool = "search", transportDigest = null, enrollmentDigest = null, hostHome = null
418
379
  }) {
419
- const scoped = sessionTimelineBinding({ host, sessionId, scope });
420
- if (!hasExactPrivateTimelineScope(scope) || !hasVerifiedTimelinePrivateScope(scope) || !completeTimelineBinding(scoped)) {
421
- return { blocked: true, reason: "session timeline scope is unavailable", authority: AUTHORITY };
422
- }
423
- const { target, wanted } = timelineQuery({ at, query });
424
- const boundedWindowSeconds = windowSeconds === undefined ? 0 : windowSeconds;
425
- if (!Number.isInteger(boundedWindowSeconds) || boundedWindowSeconds < 0 || boundedWindowSeconds > 900) {
426
- throw new Error("timeline window is invalid");
427
- }
428
- if (typeof includePriorSessions !== "boolean") throw new Error("prior session selection is invalid");
429
- if (typeof includePriorProviders !== "boolean") throw new Error("prior provider selection is invalid");
430
- if (includePriorProviders && (!includePriorSessions || !crossProviderTimelineEnabled(environment))) {
431
- return { blocked: true, reason: "cross-provider timeline recall is unavailable", authority: AUTHORITY };
432
- }
433
- try { await ensureSessionTimelineTrust(); }
434
- catch { return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY }; }
435
- let names; let state;
436
- try {
437
- names = await paths(root);
438
- state = await readState(names.path, root, names.assertStable);
439
- }
440
- catch { return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY }; }
441
- const currentSource = sourceFor(state, scoped);
442
- if (!currentSource) return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
443
- if (!await confirmedSourceEnrollment({ root, scoped, source: currentSource, hostHome })) {
444
- return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
445
- }
446
- const source = includePriorSessions
447
- ? selectPriorTimelineSource(state, scoped, { target, wanted, windowMs: boundedWindowSeconds * 1000,
448
- includePriorProviders })
449
- : currentSource;
450
- const sourceHome = source?.binding.host === scoped.host ? hostHome : source?.profileRoot;
451
- if (!source || !await pathMatchesSource(source, sourceHome)
452
- || !await confirmedSourceEnrollment({ root, scoped: source.binding, source, hostHome: sourceHome })) {
453
- return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
454
- }
455
- const sourceDigest = sourceMetadata(source).sourceDigest;
456
- if (!/^(search|capture)$/.test(invocationTool) || !invocationRequest
457
- || !await timelineTransportEnrollmentMatches({ root, binding: scoped, enrollmentDigest, transportDigest, hostHome })
458
- || !await consumeSessionTimelineInvocation({ root, tool: invocationTool, binding: scoped, sourceDigest,
459
- request: invocationRequest, transportDigest })) {
460
- return { blocked: true, reason: "session timeline invocation is unavailable", authority: AUTHORITY };
461
- }
462
- const indexed = rankTimelineEvents(source.events
463
- .filter((event) => matchesTimelineEvent(event, wanted, target, boundedWindowSeconds * 1000)), wanted, target).slice(0, 8);
464
- if (includePriorSessions && !indexed.length) {
465
- return searchResult(source, target, wanted, "prior-index", [], { priorSession: true,
466
- priorProvider: source.binding.host !== scoped.host }, invocationTool === "capture");
467
- }
468
- const opened = await validatedHandle(source, sourceHome);
469
- if (opened.status !== "open") return { blocked: true, reason: opened.reason, authority: AUTHORITY };
470
- try {
471
- const verified = [];
472
- for (const event of indexed) {
473
- const current = await verifyTimelineEvent({ handle: opened.handle, event, readRange, digest,
474
- eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host) });
475
- if (!current || !matchesTimelineEvent(current, wanted, target, boundedWindowSeconds * 1000)) return { blocked: true, reason: "timeline evidence changed", authority: AUTHORITY };
476
- verified.push(current);
477
- }
478
- if (verified.length) {
479
- if (!await unchangedHandle(opened.handle, source, sourceHome)) return { blocked: true, reason: "transcript-changed", authority: AUTHORITY };
480
- return searchResult(source, target, wanted, includePriorSessions ? "prior-verified-index" : "verified-index", verified,
481
- includePriorSessions ? { priorSession: true, priorProvider: source.binding.host !== scoped.host } : {},
482
- invocationTool === "capture");
483
- }
484
- if (!target) return searchResult(source, target, wanted, "verified-index", [], {}, invocationTool === "capture");
485
- const sought = await seekTimelineEvidence({ handle: opened.handle, size: opened.size, target, wanted,
486
- windowMs: boundedWindowSeconds * 1000, readRange,
487
- eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host),
488
- extractTimestamp: extractTimelineTimestamp,
489
- matches: matchesTimelineEvent, rank: rankTimelineEvents });
490
- if (sought.status === "searched") {
491
- if (!await unchangedHandle(opened.handle, source, sourceHome)) return { blocked: true, reason: "transcript-changed", authority: AUTHORITY };
492
- return searchResult(source, target, wanted, "timestamp-seek", sought.events,
493
- { budgetExhausted: sought.budgetExhausted }, invocationTool === "capture");
494
- }
495
- if (sought.status === "out-of-range") {
496
- return searchResult(source, target, wanted, "timestamp-seek", [], {}, invocationTool === "capture");
497
- }
498
- return { blocked: true, reason: sought.reason, authority: AUTHORITY };
499
- } finally { await opened.handle.close(); }
380
+ const scoped = sessionTimelineBinding({ host, sessionId, scope });
381
+ if (!hasExactPrivateTimelineScope(scope) || !hasVerifiedTimelinePrivateScope(scope) || !completeTimelineBinding(scoped)) {
382
+ return { blocked: true, reason: "session timeline scope is unavailable", authority: AUTHORITY };
383
+ }
384
+ const { target, wanted } = timelineQuery({ at, query });
385
+ const boundedWindowSeconds = windowSeconds === undefined ? 0 : windowSeconds;
386
+ if (!Number.isInteger(boundedWindowSeconds) || boundedWindowSeconds < 0 || boundedWindowSeconds > 900) {
387
+ throw new Error("timeline window is invalid");
388
+ }
389
+ if (typeof includePriorSessions !== "boolean") throw new Error("prior session selection is invalid");
390
+ if (typeof includePriorProviders !== "boolean") throw new Error("prior provider selection is invalid");
391
+ if (includePriorProviders && (!includePriorSessions || !crossProviderTimelineEnabled(environment))) {
392
+ return { blocked: true, reason: "cross-provider timeline recall is unavailable", authority: AUTHORITY };
393
+ }
394
+ try { await ensureSessionTimelineTrust(); }
395
+ catch { return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY }; }
396
+ let names; let state;
397
+ try {
398
+ names = await paths(root);
399
+ state = await readState(names.path, root, names.assertStable);
400
+ }
401
+ catch { return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY }; }
402
+ const currentSource = sourceFor(state, scoped);
403
+ if (!currentSource) return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
404
+ if (!await confirmedSourceEnrollment({ root, scoped, source: currentSource, hostHome })) {
405
+ return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
406
+ }
407
+ const source=includePriorSessions
408
+ ?selectPriorTimelineSource(state,scoped,{target,wanted,windowMs:boundedWindowSeconds*1000,
409
+ includePriorProviders,sessionRef:invocationRequest?.ref})
410
+ : currentSource;
411
+ const sourceHome=source?.binding.host===scoped.host?hostHome:source?.profileRoot;
412
+ if(!source||!await pathMatchesSource(source,sourceHome)
413
+ ||!await confirmedSourceEnrollment({root,scoped:source.binding,source,hostHome:sourceHome})) {
414
+ return { blocked: true, reason: "session timeline is unavailable", authority: AUTHORITY };
415
+ }
416
+ const sourceDigest=sourceMetadata(source).sourceDigest;
417
+ if(!/^(search|capture)$/.test(invocationTool)||!invocationRequest
418
+ ||!await timelineTransportEnrollmentMatches({root,binding:scoped,enrollmentDigest,transportDigest,hostHome})
419
+ ||!await consumeSessionTimelineInvocation({root,tool:invocationTool,binding:scoped,sourceDigest,
420
+ request:invocationRequest,transportDigest})) {
421
+ return { blocked: true, reason: "session timeline invocation is unavailable", authority: AUTHORITY };
422
+ }
423
+ const matchAt=invocationRequest?.ref&&target&&query==="user message"?null:target;
424
+ const indexed=rankTimelineEvents(source.events
425
+ .filter(event=>matchesTimelineEvent(event,wanted,matchAt,boundedWindowSeconds*1000)),wanted,target).slice(0,8);
426
+ if (includePriorSessions && !indexed.length) {
427
+ return searchResult(source, target, wanted, "prior-index", [], { priorSession: true,
428
+ priorProvider: source.binding.host !== scoped.host }, invocationTool === "capture");
429
+ }
430
+ const opened = await validatedHandle(source, sourceHome);
431
+ if (opened.status !== "open") return { blocked: true, reason: opened.reason, authority: AUTHORITY };
432
+ try {
433
+ const verified = [];
434
+ for (const event of indexed) {
435
+ const current = await verifyTimelineEvent({ handle: opened.handle, event, readRange, digest,
436
+ eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host) });
437
+ if(!current||!matchesTimelineEvent(current,wanted,matchAt,boundedWindowSeconds*1000)) return { blocked: true, reason: "timeline evidence changed", authority: AUTHORITY };
438
+ verified.push(current);
439
+ }
440
+ if (verified.length) {
441
+ if (!await unchangedHandle(opened.handle, source, sourceHome)) return { blocked: true, reason: "transcript-changed", authority: AUTHORITY };
442
+ return searchResult(source, target, wanted, includePriorSessions ? "prior-verified-index" : "verified-index", verified,
443
+ includePriorSessions ? { priorSession: true, priorProvider: source.binding.host !== scoped.host } : {},
444
+ invocationTool === "capture");
445
+ }
446
+ if (!target) return searchResult(source, target, wanted, "verified-index", [], {}, invocationTool === "capture");
447
+ const sought = await seekTimelineEvidence({ handle: opened.handle, size: opened.size, target, wanted,
448
+ windowMs: boundedWindowSeconds * 1000, readRange,
449
+ eventFromLine: (line, offset) => verifiedTimelineEventFromLine(line, offset, AUTHORITY, source.binding.host),
450
+ extractTimestamp: extractTimelineTimestamp,
451
+ matches: matchesTimelineEvent, rank: rankTimelineEvents });
452
+ if (sought.status === "searched") {
453
+ if (!await unchangedHandle(opened.handle, source, sourceHome)) return { blocked: true, reason: "transcript-changed", authority: AUTHORITY };
454
+ return searchResult(source, target, wanted, "timestamp-seek", sought.events,
455
+ { budgetExhausted: sought.budgetExhausted }, invocationTool === "capture");
456
+ }
457
+ if (sought.status === "out-of-range") {
458
+ return searchResult(source, target, wanted, "timestamp-seek", [], {}, invocationTool === "capture");
459
+ }
460
+ return { blocked: true, reason: sought.reason, authority: AUTHORITY };
461
+ } finally { await opened.handle.close(); }
500
462
  }