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.
- package/CHANGELOG.md +39 -0
- package/README.md +17 -1
- package/agent-spine-plugin/CHANGELOG.md +1 -1
- package/agent-spine-plugin/docs/host-integration.md +1 -15
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/scripts/check-install.js +1 -6
- package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
- package/agent-spine-plugin/scripts/release-check.js +2 -3
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
- package/agent-spine-plugin/src/hook.js +40 -40
- package/agent-spine-plugin/src/lib/briefing.js +6 -3
- package/agent-spine-plugin/src/lib/hook-context.js +63 -64
- package/agent-spine-plugin/src/lib/hook-output.js +115 -94
- package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
- package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
- package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
- package/agent-spine-plugin/src/lib/source-roots.js +26 -16
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
- package/bin/core-bootstrap.js +18 -10
- package/bin/launcher-mode.js +5 -0
- package/bin/launcher-runtime.js +27 -0
- package/bin/verify-bundled-agent-sources.cjs +28 -2
- package/blun.mjs +798 -143
- package/bundled-agent-sources.json +192 -155
- package/package.json +1 -1
- package/standard-skills/translate-native/README.md +1334 -1
- package/standard-skills/translate-native/SKILL.md +1 -1
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
- package/standard-skills/translate-native/integrations/website_localization.py +234 -10
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
- package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
- package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
- package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
- package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
- package/standard-skills/translate-native/provenance.json +2 -2
- package/standard-skills/translate-native/references/commercial-localization.md +96 -17
- package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/VERSION +1 -1
- package/standard-tools/language-guard/blun_language_guard.py +3 -1
- package/standard-tools/language-guard/check_commercial_review.py +69 -9
- package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/provenance.json +2 -2
- package/standard-tools/manifest.json +1 -1
- package/worker-host.mjs +215 -82
- 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
|
-
|
|
7
|
-
|
|
6
|
+
completeTimelineBinding, hasVerifiedTimelinePrivateScope, sameTimelineBinding, sessionTimelineBinding,
|
|
7
|
+
TIMELINE_ID_RE, validTimelineBinding
|
|
8
8
|
} from "./session-timeline-contract.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
ensureSessionTimelineTrust, sessionTimelineStatePaths
|
|
11
11
|
} from "./session-timeline-auth.js";
|
|
12
12
|
import { consumeSessionTimelineInvocation, issueSessionTimelineInvocation } from "./session-timeline-invocation.js";
|
|
13
13
|
import {
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
101
|
+
return readTimelineState({ path, root, maximumBytes: MAX_STATE_BYTES, empty, validate, assertStable });
|
|
106
102
|
}
|
|
107
103
|
function saveState(state, path, assertOwned, root, assertStable) {
|
|
108
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
145
|
+
root, enrollmentDigest, environment = process.env, now = new Date()
|
|
157
146
|
}) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
167
|
+
root, host, sessionId, scope, environment = process.env
|
|
183
168
|
}) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
195
|
+
return status({ status: "unavailable", reason: "timeline-enrollment-bootstrap-required" });
|
|
218
196
|
}
|
|
219
|
-
|
|
220
197
|
function mergeEvents(existing, additions) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
296
|
-
|
|
269
|
+
root, host, sessionId, scope, maxBytes = 4 * 1024 * 1024,
|
|
270
|
+
invocationRequest = null, transportDigest = null, enrollmentDigest = null, hostHome = null
|
|
297
271
|
}) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
334
|
-
|
|
299
|
+
root, host, sessionId, scope, hostHome, tool, request, toolUseId, transportDigest, enrollmentDigest,
|
|
300
|
+
environment = process.env
|
|
335
301
|
}) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
368
|
-
|
|
330
|
+
root, host, sessionId, scope, references, includePriorProviders = false,
|
|
331
|
+
environment = process.env, hostHome = null
|
|
369
332
|
}) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
}
|