@skill-harness/adapters 0.16.0 → 0.17.0

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/dist/index.d.ts CHANGED
@@ -32,3 +32,5 @@ export * from "./archive-access.js";
32
32
  export * from "./archive-facts.js";
33
33
  export * from "./trust-lifecycle.js";
34
34
  export * from "./producer-review.js";
35
+ export * from './session-observation.js';
36
+ export * from './session-retention.js';
package/dist/index.js CHANGED
@@ -44,4 +44,6 @@ export * from "./archive-access.js";
44
44
  export * from "./archive-facts.js";
45
45
  export * from "./trust-lifecycle.js";
46
46
  export * from "./producer-review.js";
47
+ export * from './session-observation.js';
48
+ export * from './session-retention.js';
47
49
  //# sourceMappingURL=index.js.map
@@ -51,6 +51,68 @@ export interface LearningRegistryReceipt {
51
51
  * exposure, registry mutation, authentication or hostile-same-UID protection. */
52
52
  export declare function createLearningWorkspace(directory: string, input: LearningWorkspaceInput): {
53
53
  configuration: () => LearningWorkspaceInput;
54
+ bindSession(raw: {
55
+ name: string;
56
+ title: string;
57
+ manifestId: string;
58
+ }): {
59
+ name: string;
60
+ title: string;
61
+ manifestId: string;
62
+ };
63
+ noteSession(name: string, raw: {
64
+ kind: "finding" | "proposal" | "acceptance" | "context";
65
+ note: string;
66
+ item: number;
67
+ line: number;
68
+ }): {
69
+ author: string;
70
+ kind: "finding" | "proposal" | "acceptance" | "context";
71
+ note: string;
72
+ item: number;
73
+ line: number;
74
+ };
75
+ sessions(): ({
76
+ state: string;
77
+ sessionId: string;
78
+ reason: string;
79
+ name: string;
80
+ title: string;
81
+ manifestId: string;
82
+ } | {
83
+ state: string;
84
+ sessionId: null;
85
+ reason: string;
86
+ name: string;
87
+ title: string;
88
+ manifestId: string;
89
+ })[];
90
+ session(name: string): {
91
+ annotations: {
92
+ kind: string;
93
+ note: string;
94
+ item: number;
95
+ line: number;
96
+ author: string;
97
+ }[];
98
+ acceptance: "not-assessed";
99
+ parent: import("./session-observation.js").SessionObservation;
100
+ children: {
101
+ source: number;
102
+ executionId: string | null;
103
+ session: import("./session-observation.js").SessionObservation;
104
+ }[];
105
+ attempts: import("./session-observation.js").SessionAttempt[];
106
+ gaps: string[];
107
+ version: "retrospective-session-v1";
108
+ manifestId: string;
109
+ selectionDigest: string;
110
+ sources: (import("./session-retention.js").Source & {
111
+ manifestId: string;
112
+ })[];
113
+ name: string;
114
+ title: string;
115
+ };
54
116
  dashboardBinding(): {
55
117
  trustDirectory: string | null;
56
118
  trustPolicyId: string | null;
@@ -404,6 +466,21 @@ export declare function createLearningWorkspace(directory: string, input: Learni
404
466
  version: "learning-workspace-view-v1";
405
467
  configuration: LearningWorkspaceInput;
406
468
  mode: "deliberate-review";
469
+ sessions: ({
470
+ state: string;
471
+ sessionId: string;
472
+ reason: string;
473
+ name: string;
474
+ title: string;
475
+ manifestId: string;
476
+ } | {
477
+ state: string;
478
+ sessionId: null;
479
+ reason: string;
480
+ name: string;
481
+ title: string;
482
+ manifestId: string;
483
+ })[];
407
484
  cases: ({
408
485
  name: string;
409
486
  title: string;
@@ -485,6 +562,68 @@ export declare function createLearningWorkspace(directory: string, input: Learni
485
562
  };
486
563
  export declare function openLearningWorkspace(directory: string): {
487
564
  configuration: () => LearningWorkspaceInput;
565
+ bindSession(raw: {
566
+ name: string;
567
+ title: string;
568
+ manifestId: string;
569
+ }): {
570
+ name: string;
571
+ title: string;
572
+ manifestId: string;
573
+ };
574
+ noteSession(name: string, raw: {
575
+ kind: "finding" | "proposal" | "acceptance" | "context";
576
+ note: string;
577
+ item: number;
578
+ line: number;
579
+ }): {
580
+ author: string;
581
+ kind: "finding" | "proposal" | "acceptance" | "context";
582
+ note: string;
583
+ item: number;
584
+ line: number;
585
+ };
586
+ sessions(): ({
587
+ state: string;
588
+ sessionId: string;
589
+ reason: string;
590
+ name: string;
591
+ title: string;
592
+ manifestId: string;
593
+ } | {
594
+ state: string;
595
+ sessionId: null;
596
+ reason: string;
597
+ name: string;
598
+ title: string;
599
+ manifestId: string;
600
+ })[];
601
+ session(name: string): {
602
+ annotations: {
603
+ kind: string;
604
+ note: string;
605
+ item: number;
606
+ line: number;
607
+ author: string;
608
+ }[];
609
+ acceptance: "not-assessed";
610
+ parent: import("./session-observation.js").SessionObservation;
611
+ children: {
612
+ source: number;
613
+ executionId: string | null;
614
+ session: import("./session-observation.js").SessionObservation;
615
+ }[];
616
+ attempts: import("./session-observation.js").SessionAttempt[];
617
+ gaps: string[];
618
+ version: "retrospective-session-v1";
619
+ manifestId: string;
620
+ selectionDigest: string;
621
+ sources: (import("./session-retention.js").Source & {
622
+ manifestId: string;
623
+ })[];
624
+ name: string;
625
+ title: string;
626
+ };
488
627
  dashboardBinding(): {
489
628
  trustDirectory: string | null;
490
629
  trustPolicyId: string | null;
@@ -838,6 +977,21 @@ export declare function openLearningWorkspace(directory: string): {
838
977
  version: "learning-workspace-view-v1";
839
978
  configuration: LearningWorkspaceInput;
840
979
  mode: "deliberate-review";
980
+ sessions: ({
981
+ state: string;
982
+ sessionId: string;
983
+ reason: string;
984
+ name: string;
985
+ title: string;
986
+ manifestId: string;
987
+ } | {
988
+ state: string;
989
+ sessionId: null;
990
+ reason: string;
991
+ name: string;
992
+ title: string;
993
+ manifestId: string;
994
+ })[];
841
995
  cases: ({
842
996
  name: string;
843
997
  title: string;
@@ -1,3 +1,4 @@
1
+ import { readRetainedSession, readRetainedSessionSource } from './session-retention.js';
1
2
  import { mkdirSync, readdirSync, lstatSync } from 'node:fs';
2
3
  import { dirname, isAbsolute, join, resolve } from 'node:path';
3
4
  import { buildHypothesis, buildAdoptionBinding, authorizeAdoption, authorizeRollback, buildRollbackRequest, classifyProductionObservation, } from '@skill-harness/core';
@@ -120,7 +121,7 @@ export function openLearningWorkspace(directory) {
120
121
  const history = () => {
121
122
  verifyLearningStore(input.archiveRoot, 'workspace', workspaceKey(input), directory, learningHash(initial));
122
123
  const rows = journal.read(), names = new Set(), decisions = new Map();
123
- const known = new Set(['case-binding', 'comparison-binding', 'comparison-context', 'hypothesis-binding', 'unflagged-binding', 'trust-binding', 'quality-pending', 'quality', 'decision', 'adoption', 'activation', 'rollback-request', 'rollback', 'outcome', 'lifecycle']);
124
+ const known = new Set(['session-note', 'session-binding', 'case-binding', 'comparison-binding', 'comparison-context', 'hypothesis-binding', 'unflagged-binding', 'trust-binding', 'quality-pending', 'quality', 'decision', 'adoption', 'activation', 'rollback-request', 'rollback', 'outcome', 'lifecycle']);
124
125
  for (const row of rows.slice(1)) {
125
126
  const v = row.value;
126
127
  if (!known.has(String(v.type)))
@@ -313,6 +314,47 @@ export function openLearningWorkspace(directory) {
313
314
  history();
314
315
  return {
315
316
  configuration: () => learningCopy(input),
317
+ bindSession(raw) {
318
+ const b = learningCopy(raw);
319
+ closed(b, ['name', 'title', 'manifestId']);
320
+ named(b);
321
+ hash(b.manifestId);
322
+ readRetainedSession(input.archiveRoot, b.manifestId);
323
+ bound('session-binding', b);
324
+ return b;
325
+ },
326
+ noteSession(name, raw) {
327
+ const a = learningCopy(raw);
328
+ closed(a, ['kind', 'note', 'item', 'line']);
329
+ text(a.note);
330
+ if (!['finding', 'proposal', 'acceptance', 'context'].includes(a.kind) || !Number.isSafeInteger(a.item) || a.item < 1 || !Number.isSafeInteger(a.line) || a.line < 1)
331
+ throw Error('valid annotation and evidence reference required');
332
+ const b = selected('session-binding', name);
333
+ const bytes = readRetainedSessionSource(input.archiveRoot, b.manifestId, a.item - 1);
334
+ const content = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
335
+ const count = content.length === 0 ? 0 : content.split('\n').length - (content.endsWith('\n') ? 1 : 0);
336
+ if (a.line > count)
337
+ throw Error('annotation evidence line outside retained source');
338
+ const annotation = { ...a, author: input.author };
339
+ append({ type: 'session-note', name, annotation });
340
+ return annotation;
341
+ },
342
+ sessions() {
343
+ return bindings('session-binding').map(b => {
344
+ try {
345
+ const view = readRetainedSession(input.archiveRoot, b.manifestId);
346
+ return { ...b, state: 'ready', sessionId: view.parent.sessionId, reason: 'retrospective evidence; assessment pending' };
347
+ }
348
+ catch (e) {
349
+ return { ...b, state: 'deferred', sessionId: null, reason: message(e) };
350
+ }
351
+ });
352
+ },
353
+ session(name) {
354
+ const b = selected('session-binding', name);
355
+ return { ...b, ...readRetainedSession(input.archiveRoot, b.manifestId),
356
+ annotations: events().filter(e => e.type === 'session-note' && e.name === name).map(e => e.annotation) };
357
+ },
316
358
  dashboardBinding() {
317
359
  const trust = currentTrust(), binding = events().filter(e => e.type === 'trust-binding').at(-1);
318
360
  return { trustDirectory: trust && binding ? String(binding.directory) : null, trustPolicyId: trust ? trust.inspect(0).policyId : null };
@@ -616,7 +658,7 @@ export function openLearningWorkspace(directory) {
616
658
  catch (e) {
617
659
  trust = { state: 'deferred', reason: message(e), automaticExposure: { mode: 'silent', reason: 'invalid-trust-evidence' } };
618
660
  }
619
- return { version: 'learning-workspace-view-v1', configuration: learningCopy(input), mode: 'deliberate-review', cases, comparisons, trust, automaticExposureReserved: false, delegationStarted: false, acceptance: 'not-assessed' };
661
+ return { version: 'learning-workspace-view-v1', configuration: learningCopy(input), mode: 'deliberate-review', sessions: this.sessions(), cases, comparisons, trust, automaticExposureReserved: false, delegationStarted: false, acceptance: 'not-assessed' };
620
662
  },
621
663
  };
622
664
  }
@@ -0,0 +1,50 @@
1
+ type Row = Record<string, any>;
2
+ export interface SessionTimelineEntry {
3
+ line: number;
4
+ timestamp: string | null;
5
+ kind: string;
6
+ excerpt: string;
7
+ }
8
+ export interface SessionObservation {
9
+ version: 'pi-session-observation-v1';
10
+ sessionId: string;
11
+ cwd: string;
12
+ timeline: SessionTimelineEntry[];
13
+ toolCalls: Record<string, number>;
14
+ userMessages: number;
15
+ assistantMessages: number;
16
+ modelRecords: {
17
+ provider: string;
18
+ model: string;
19
+ messages: number;
20
+ }[];
21
+ usage: {
22
+ fields: Record<string, {
23
+ sum: number;
24
+ reportedMessages: number;
25
+ }>;
26
+ assistantMessages: number;
27
+ scope: 'this-transcript-only';
28
+ };
29
+ lastActivity: string | null;
30
+ lastAssistantStop: string | null;
31
+ gaps: string[];
32
+ }
33
+ export declare function sessionText(bytes: Uint8Array): string;
34
+ export declare function sessionRows(bytes: Uint8Array): Row[];
35
+ /** Chronological visible history, including abandoned branches. Never interprets prose as approval. */
36
+ export declare function observePiSession(bytes: Uint8Array): SessionObservation;
37
+ export interface SessionAttempt {
38
+ executionId: string;
39
+ parentExecutionId: string | null;
40
+ logicalChildId: string;
41
+ agent: string | null;
42
+ state: string;
43
+ pane: string | null;
44
+ startedAt: string | null;
45
+ endedAt: string | null;
46
+ runningSeconds: number | null;
47
+ }
48
+ export declare function observeSessionLedger(bytes: Uint8Array): SessionAttempt[];
49
+ export declare function sessionScope(observation: SessionObservation, sourceSha256: string): string;
50
+ export {};
@@ -0,0 +1,186 @@
1
+ import { learningHash } from './learning-journal.js';
2
+ import { normalizePiDaddyLedgerV3 } from './trajectory.js';
3
+ export function sessionText(bytes) {
4
+ if (bytes.byteLength > 8 * 1024 * 1024)
5
+ throw Error('session source exceeds 8 MiB');
6
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
7
+ }
8
+ export function sessionRows(bytes) {
9
+ const text = sessionText(bytes);
10
+ if (!text.endsWith('\n'))
11
+ throw Error('incomplete session/ledger line; retry after the writer settles');
12
+ const lines = text.slice(0, -1).split('\n');
13
+ if (lines.length > 4096)
14
+ throw Error('session source exceeds 4096 records');
15
+ return lines.map((line, i) => {
16
+ let row;
17
+ try {
18
+ row = JSON.parse(line);
19
+ }
20
+ catch {
21
+ throw Error('invalid JSON record at line ' + (i + 1));
22
+ }
23
+ if (!row || typeof row !== 'object' || Array.isArray(row))
24
+ throw Error('object record required at line ' + (i + 1));
25
+ return row;
26
+ });
27
+ }
28
+ function value(v, max = 512) {
29
+ if (typeof v !== 'string' || !v.trim() || v.length > max)
30
+ throw Error('bounded session identifier required');
31
+ return v;
32
+ }
33
+ const stamp = (v) => typeof v === 'string' && Number.isFinite(Date.parse(v)) ? v : null;
34
+ const excerpt = (v) => v.length > 512 ? v.slice(0, 512) + ' … [excerpt; inspect retained source for complete text]' : v;
35
+ /** Chronological visible history, including abandoned branches. Never interprets prose as approval. */
36
+ export function observePiSession(bytes) {
37
+ const rows = sessionRows(bytes), header = rows[0];
38
+ if (header?.type !== 'session' || header.version !== 3)
39
+ throw Error('Pi session format version 3 required');
40
+ const sessionId = value(header.id), cwd = value(header.cwd, 4096), ids = new Set();
41
+ const timeline = [], tools = new Map(), models = new Map();
42
+ const fields = {};
43
+ let userMessages = 0, assistantMessages = 0, lastAssistantStop = null, lastActivity = null, opaque = 0;
44
+ const add = (i, row, kind, text) => timeline.push({ line: i + 1, timestamp: stamp(row.timestamp), kind, excerpt: excerpt(text) });
45
+ rows.forEach((row, i) => {
46
+ if (i && row.type === 'session')
47
+ throw Error('multiple session headers');
48
+ if (row.id !== undefined) {
49
+ const id = value(row.id);
50
+ if (ids.has(id))
51
+ throw Error('duplicate session record id');
52
+ ids.add(id);
53
+ }
54
+ lastActivity = stamp(row.timestamp) ?? lastActivity;
55
+ if (row.type === 'model_change') {
56
+ add(i, row, 'model-setting', [row.provider, row.modelId].filter(x => typeof x === 'string').join('/'));
57
+ return;
58
+ }
59
+ if (row.type === 'thinking_level_change') {
60
+ add(i, row, 'thinking-setting', String(row.thinkingLevel ?? 'unknown'));
61
+ return;
62
+ }
63
+ if (row.type !== 'message')
64
+ return;
65
+ const m = row.message;
66
+ if (!m || typeof m !== 'object' || Array.isArray(m))
67
+ throw Error('invalid session message');
68
+ const content = typeof m.content === 'string' ? [{ type: 'text', text: m.content }] : m.content;
69
+ if (!Array.isArray(content))
70
+ throw Error('invalid session message content');
71
+ const text = content.filter(c => c?.type === 'text' && typeof c.text === 'string').map(c => c.text).join('\n');
72
+ if (m.role === 'user') {
73
+ userMessages++;
74
+ add(i, row, 'user-message', text);
75
+ }
76
+ else if (m.role === 'assistant') {
77
+ assistantMessages++;
78
+ lastAssistantStop = typeof m.stopReason === 'string' ? m.stopReason : null;
79
+ const provider = typeof m.provider === 'string' ? m.provider : 'unknown', model = typeof m.model === 'string' ? m.model : 'unknown';
80
+ const key = JSON.stringify([provider, model]), record = models.get(key) ?? { provider, model, messages: 0 };
81
+ record.messages++;
82
+ models.set(key, record);
83
+ if (text)
84
+ add(i, row, 'assistant-message', text);
85
+ for (const field of ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens']) {
86
+ const n = m.usage?.[field];
87
+ if (n === undefined)
88
+ continue;
89
+ if (!Number.isSafeInteger(n) || n < 0)
90
+ throw Error('invalid provider-reported usage');
91
+ const f = fields[field] ?? { sum: 0, reportedMessages: 0 };
92
+ f.sum += n;
93
+ f.reportedMessages++;
94
+ if (!Number.isSafeInteger(f.sum))
95
+ throw Error('usage sum overflow');
96
+ fields[field] = f;
97
+ }
98
+ for (const c of content)
99
+ if (c?.type === 'toolCall') {
100
+ const name = value(c.name, 128);
101
+ tools.set(name, (tools.get(name) ?? 0) + 1);
102
+ add(i, row, 'tool-call', name);
103
+ }
104
+ }
105
+ else if (m.role === 'toolResult')
106
+ add(i, row, 'tool-result', String(m.toolName ?? 'unknown') + (m.isError === true ? ' — tool reported error' : ' — result recorded; success not inferred'));
107
+ else
108
+ opaque++;
109
+ });
110
+ const gaps = [
111
+ 'Retrospective chronological transcript; branch selection and live continuity are not established.',
112
+ 'Messages and tool results are evidence to inspect, not independently verified decisions, tests or acceptance.',
113
+ 'Only explicit inputs are captured. Source revision, initial workspace and external user feedback may be missing.',
114
+ 'Usage is provider-reported repeated-request usage, not unique context, billing or complete parent/child accounting.',
115
+ ];
116
+ if (opaque)
117
+ gaps.push(opaque + ' message(s) with other roles omitted from the timeline; retained source is complete.');
118
+ if (!assistantMessages || Object.values(fields).some(f => f.reportedMessages < assistantMessages) || Object.keys(fields).length < 5)
119
+ gaps.push('Usage fields are missing; sums cover only reported messages.');
120
+ if (lastAssistantStop !== 'stop')
121
+ gaps.push('No final assistant stop observed; this snapshot may represent unfinished work.');
122
+ return { version: 'pi-session-observation-v1', sessionId, cwd, timeline, toolCalls: Object.fromEntries(tools), userMessages, assistantMessages, modelRecords: [...models.values()], usage: { fields, assistantMessages, scope: 'this-transcript-only' }, lastActivity, lastAssistantStop, gaps };
123
+ }
124
+ export function observeSessionLedger(bytes) {
125
+ const rows = sessionRows(bytes);
126
+ normalizePiDaddyLedgerV3(sessionText(bytes));
127
+ const attempts = new Map(), decisions = new Set(), lifecycles = new Set(), deadlines = new Map(), displayed = new Set();
128
+ for (const row of rows) {
129
+ if (!['capability_decision', 'child_lifecycle', 'workspace_lease'].includes(row.event) || String(row.childId).startsWith('check:'))
130
+ continue;
131
+ const id = value(row.executionId), old = attempts.get(id);
132
+ if (old && (old.parentExecutionId !== row.parentExecutionId || old.logicalChildId !== row.childId))
133
+ throw Error('execution identity changed');
134
+ const a = old ?? { executionId: id, parentExecutionId: row.parentExecutionId, logicalChildId: row.childId, agent: null, state: 'unknown', pane: null, startedAt: null, endedAt: null, runningSeconds: null };
135
+ if (row.event === 'workspace_lease') {
136
+ attempts.set(id, a);
137
+ continue;
138
+ }
139
+ displayed.add(id);
140
+ if (row.event === 'capability_decision') {
141
+ if (decisions.has(id))
142
+ throw Error('duplicate capability decision for one execution');
143
+ decisions.add(id);
144
+ a.agent = row.agentType;
145
+ a.state = row.blocked ? 'refused' : a.state === 'unknown' ? 'authorised' : a.state;
146
+ }
147
+ else {
148
+ if (a.endedAt)
149
+ throw Error('lifecycle after terminal execution');
150
+ if (lifecycles.has(id) && row.state === 'starting')
151
+ throw Error('duplicate starting lifecycle event');
152
+ if (deadlines.has(id) && stamp(row.deadlineAt) && deadlines.get(id) !== row.deadlineAt)
153
+ throw Error('lifecycle deadline changed');
154
+ if (stamp(row.deadlineAt))
155
+ deadlines.set(id, row.deadlineAt);
156
+ lifecycles.add(id);
157
+ a.state = row.state;
158
+ a.pane = row.herdrPaneId ?? a.pane;
159
+ if (row.state === 'running')
160
+ a.startedAt = a.startedAt ?? row.ts;
161
+ if (['completed', 'failed'].includes(row.state))
162
+ a.endedAt = row.ts;
163
+ }
164
+ if (a.startedAt && a.endedAt) {
165
+ a.runningSeconds = (Date.parse(a.endedAt) - Date.parse(a.startedAt)) / 1000;
166
+ if (a.runningSeconds < 0)
167
+ throw Error('negative execution duration');
168
+ }
169
+ attempts.set(id, a);
170
+ }
171
+ for (const a of attempts.values()) {
172
+ const seen = new Set();
173
+ let cursor = a;
174
+ while (cursor) {
175
+ if (seen.has(cursor.executionId))
176
+ throw Error('execution parent cycle');
177
+ seen.add(cursor.executionId);
178
+ cursor = cursor.parentExecutionId ? attempts.get(cursor.parentExecutionId) : undefined;
179
+ }
180
+ }
181
+ return [...attempts.values()].filter(a => displayed.has(a.executionId));
182
+ }
183
+ export function sessionScope(observation, sourceSha256) {
184
+ return learningHash({ kind: 'retrospective-pi-session', sessionId: observation.sessionId, sourceSha256 });
185
+ }
186
+ //# sourceMappingURL=session-observation.js.map
@@ -0,0 +1,48 @@
1
+ import { observeSessionLedger, type SessionObservation } from './session-observation.js';
2
+ export interface SessionSourceSelection {
3
+ kind: 'parent' | 'child' | 'ledger' | 'feedback' | 'artifact';
4
+ path: string;
5
+ executionId?: string;
6
+ }
7
+ export interface Source {
8
+ kind: SessionSourceSelection['kind'];
9
+ path: string;
10
+ sha256: string;
11
+ bytes: number;
12
+ executionId: string | null;
13
+ }
14
+ export interface SessionImportPreview {
15
+ version: 'session-import-preview-v1';
16
+ digest: string;
17
+ sources: Source[];
18
+ parent: SessionObservation;
19
+ children: {
20
+ source: number;
21
+ executionId: string | null;
22
+ session: SessionObservation;
23
+ }[];
24
+ attempts: ReturnType<typeof observeSessionLedger>;
25
+ gaps: string[];
26
+ acceptance: 'not-assessed';
27
+ }
28
+ /** Explicit local selection only. Does not scan session directories, follow links, or contact workers. */
29
+ export declare function prepareSessionImport(selections: SessionSourceSelection[]): SessionImportPreview;
30
+ export declare function retainSessionImport(root: string, preview: SessionImportPreview, expectedDigest: string): string;
31
+ export declare function readRetainedSession(root: string, manifestId: string): {
32
+ acceptance: "not-assessed";
33
+ parent: SessionObservation;
34
+ children: {
35
+ source: number;
36
+ executionId: string | null;
37
+ session: SessionObservation;
38
+ }[];
39
+ attempts: import("./session-observation.js").SessionAttempt[];
40
+ gaps: string[];
41
+ version: "retrospective-session-v1";
42
+ manifestId: string;
43
+ selectionDigest: string;
44
+ sources: (Source & {
45
+ manifestId: string;
46
+ })[];
47
+ };
48
+ export declare function readRetainedSessionSource(root: string, manifestId: string, index: number): Buffer;
@@ -0,0 +1,121 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+ import { learningFile, learningHash, learningCopy } from './learning-journal.js';
4
+ import { retainArchiveSource, readArchiveSource } from './evidence-archive.js';
5
+ import { observePiSession, observeSessionLedger, sessionText } from './session-observation.js';
6
+ const captured = new WeakMap();
7
+ const sha = (bytes) => createHash('sha256').update(bytes).digest('hex');
8
+ function checkedSelections(input) {
9
+ if (!Array.isArray(input) || input.length < 1 || input.length > 16)
10
+ throw Error('select 1–16 explicit session sources');
11
+ const seen = new Set(), executions = new Set();
12
+ let parents = 0, ledgers = 0;
13
+ for (const s of input) {
14
+ if (!s || typeof s !== 'object' || Object.keys(s).some(k => !['kind', 'path', 'executionId'].includes(k)))
15
+ throw Error('closed source selection required');
16
+ if (!['parent', 'child', 'ledger', 'feedback', 'artifact'].includes(s.kind) || typeof s.path !== 'string' || resolve(s.path) !== s.path)
17
+ throw Error('explicit absolute source path required');
18
+ if (seen.has(s.path))
19
+ throw Error('duplicate session source');
20
+ seen.add(s.path);
21
+ if (s.kind === 'parent')
22
+ parents++;
23
+ if (s.kind === 'ledger')
24
+ ledgers++;
25
+ if (s.executionId !== undefined) {
26
+ if (s.kind !== 'child' || !/^exec:[0-9a-f-]{36}$/i.test(s.executionId) || executions.has(s.executionId))
27
+ throw Error('unique child execution id required');
28
+ executions.add(s.executionId);
29
+ }
30
+ }
31
+ if (parents !== 1 || ledgers > 1)
32
+ throw Error('exactly one parent and at most one ledger required');
33
+ }
34
+ function projectSession(selections, buffers) {
35
+ const parent = observePiSession(buffers[selections.findIndex(s => s.kind === 'parent')]);
36
+ const children = selections.flatMap((s, i) => s.kind === 'child' ? [{ source: i, executionId: s.executionId ?? null, session: observePiSession(buffers[i]) }] : []);
37
+ if (new Set([parent.sessionId, ...children.map(c => c.session.sessionId)]).size !== children.length + 1)
38
+ throw Error('duplicate parent/child session id');
39
+ const ledger = selections.findIndex(s => s.kind === 'ledger'), attempts = ledger < 0 ? [] : observeSessionLedger(buffers[ledger]);
40
+ for (const child of children)
41
+ if (child.executionId && !attempts.some(a => a.executionId === child.executionId))
42
+ throw Error('selected child execution absent from ledger');
43
+ const gaps = [...parent.gaps, 'No quality, acceptance, calibrated learning or adoption is inferred from this import.'];
44
+ for (const child of children)
45
+ gaps.push(...child.session.gaps.map(g => 'Child source ' + (child.source + 1) + ' (' + child.session.sessionId + '): ' + g));
46
+ if (ledger < 0)
47
+ gaps.push('No delegation ledger selected.');
48
+ else
49
+ gaps.push('Ledger/session association is operator-selected, not authenticated; ledger may include other sessions.');
50
+ if (attempts.length > children.filter(c => c.executionId).length)
51
+ gaps.push('Child transcript coverage incomplete; no combined token total is claimed.');
52
+ if (children.length)
53
+ gaps.push('Child execution mappings are explicit operator declarations, not proof of parentage. Each transcript has separate usage.');
54
+ if (!selections.some(s => s.kind === 'feedback'))
55
+ gaps.push('External feedback not supplied; user messages remain available for review.');
56
+ if (!selections.some(s => s.kind === 'artifact'))
57
+ gaps.push('No final artifact or source snapshot supplied.');
58
+ return { parent, children, attempts, gaps };
59
+ }
60
+ /** Explicit local selection only. Does not scan session directories, follow links, or contact workers. */
61
+ export function prepareSessionImport(selections) {
62
+ checkedSelections(selections);
63
+ const buffers = selections.map(s => learningFile(s.path, 8 * 1024 * 1024));
64
+ if (buffers.reduce((n, b) => n + b.length, 0) > 32 * 1024 * 1024)
65
+ throw Error('session selection exceeds 32 MiB');
66
+ const sources = selections.map((s, i) => ({ ...s, executionId: s.executionId ?? null, sha256: sha(buffers[i]), bytes: buffers[i].length }));
67
+ const { parent, children, attempts, gaps } = projectSession(selections, buffers);
68
+ const digest = learningHash({ version: 'session-selection-v1', sources });
69
+ const preview = { version: 'session-import-preview-v1', digest, sources, parent, children, attempts, gaps, acceptance: 'not-assessed' };
70
+ captured.set(preview, { preview: learningCopy(preview), buffers });
71
+ return preview;
72
+ }
73
+ export function retainSessionImport(root, preview, expectedDigest) {
74
+ const c = captured.get(preview);
75
+ if (!c || learningHash(c.preview) !== learningHash(preview) || expectedDigest !== c.preview.digest)
76
+ throw Error('exact preview digest required; changed or forged session import');
77
+ for (const s of c.preview.sources)
78
+ if (sha(learningFile(s.path, 8 * 1024 * 1024)) !== s.sha256)
79
+ throw Error('session source changed since preview; prepare again');
80
+ const sources = c.preview.sources.map((s, i) => {
81
+ const parser = s.kind === 'parent' || s.kind === 'child' ? 'pi-session-jsonl' : s.kind === 'ledger' ? 'pi-daddy-session-ledger' : 'session-evidence';
82
+ const r = retainArchiveSource(root, { sourceId: 'session-' + s.sha256, parser: { id: parser, version: '1' }, retention: 'exact', bytes: c.buffers[i] });
83
+ return { ...s, manifestId: r.manifestId };
84
+ });
85
+ const record = { version: 'retrospective-session-v1', selectionDigest: preview.digest, sources };
86
+ return retainArchiveSource(root, { sourceId: 'retrospective-' + preview.digest, parser: { id: 'retrospective-session', version: '1' }, retention: 'exact', bytes: Buffer.from(JSON.stringify(record)) }).manifestId;
87
+ }
88
+ export function readRetainedSession(root, manifestId) {
89
+ const selected = readArchiveSource(root, manifestId);
90
+ if (selected.status !== 'available' || selected.reference.retention !== 'exact' || selected.reference.parser.id !== 'retrospective-session' || selected.reference.parser.version !== '1')
91
+ throw Error('retained session unavailable or unsupported');
92
+ const record = JSON.parse(sessionText(selected.bytes));
93
+ if (!record || record.version !== 'retrospective-session-v1' || Object.keys(record).sort().join() !== 'selectionDigest,sources,version' || !Array.isArray(record.sources))
94
+ throw Error('invalid retained session');
95
+ checkedSelections(record.sources.map(s => ({ kind: s.kind, path: s.path, ...(s.executionId === null ? {} : { executionId: s.executionId }) })));
96
+ const sources = record.sources.map(s => {
97
+ if (Object.keys(s).sort().join() !== 'bytes,executionId,kind,manifestId,path,sha256')
98
+ throw Error('invalid retained session source');
99
+ const r = readArchiveSource(root, s.manifestId);
100
+ if (r.status !== 'available' || r.reference.retention !== 'exact' || r.reference.sha256 !== s.sha256 || r.bytes.length !== s.bytes)
101
+ throw Error('retained session source missing or changed');
102
+ return { selection: s, bytes: r.bytes };
103
+ });
104
+ if (sources.reduce((n, s) => n + s.bytes.length, 0) > 32 * 1024 * 1024)
105
+ throw Error('session selection exceeds 32 MiB');
106
+ const plain = record.sources.map(({ manifestId, ...s }) => s);
107
+ if (learningHash({ version: 'session-selection-v1', sources: plain }) !== record.selectionDigest)
108
+ throw Error('retained session selection mismatch');
109
+ const projection = projectSession(record.sources.map(s => ({ kind: s.kind, path: s.path, ...(s.executionId === null ? {} : { executionId: s.executionId }) })), sources.map(s => s.bytes));
110
+ return { version: record.version, manifestId, selectionDigest: record.selectionDigest, sources: record.sources, ...projection, acceptance: 'not-assessed' };
111
+ }
112
+ export function readRetainedSessionSource(root, manifestId, index) {
113
+ const session = readRetainedSession(root, manifestId), source = session.sources[index];
114
+ if (!Number.isSafeInteger(index) || !source)
115
+ throw Error('select a listed session source');
116
+ const result = readArchiveSource(root, source.manifestId);
117
+ if (result.status !== 'available')
118
+ throw Error('session source unavailable');
119
+ return result.bytes;
120
+ }
121
+ //# sourceMappingURL=session-retention.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skill-harness/adapters",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "skill-harness harness adapters — pi runner + claude-code judge routing (internal API)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "prepack": "node ../../scripts/release-pack.mjs --guard"
41
41
  },
42
42
  "dependencies": {
43
- "@skill-harness/core": "0.16.0",
43
+ "@skill-harness/core": "0.17.0",
44
44
  "typebox": "1.3.4"
45
45
  }
46
46
  }