@jinn-network/client 0.2.0-canary.0b79cd5d → 0.2.0-canary.31247316

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 (31) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/build-meta.json +1 -1
  3. package/dist/vendor/@jinn-network/core/package.json +2 -2
  4. package/dist/vendor/@jinn-network/plugin/dist/index.d.ts +1 -1
  5. package/dist/vendor/@jinn-network/plugin/dist/pickup.d.ts +3 -12
  6. package/dist/vendor/@jinn-network/plugin/dist/pickup.js +9 -45
  7. package/dist/vendor/@jinn-network/plugin/dist/plugin.d.ts +1 -5
  8. package/dist/vendor/@jinn-network/plugin/dist/plugin.js +32 -121
  9. package/dist/vendor/@jinn-network/plugin/dist/ports/corpus-port.d.ts +0 -5
  10. package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-hit.d.ts +0 -2
  11. package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-hit.js +1 -9
  12. package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-packet.d.ts +4 -5
  13. package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-packet.js +7 -14
  14. package/dist/vendor/@jinn-network/plugin/dist/testing/in-memory-corpus.d.ts +1 -3
  15. package/dist/vendor/@jinn-network/plugin/dist/testing/in-memory-corpus.js +1 -5
  16. package/dist/vendor/@jinn-network/plugin/package.json +1 -1
  17. package/node_modules/@jinn-network/core/package.json +1 -1
  18. package/node_modules/@jinn-network/plugin/dist/index.d.ts +1 -1
  19. package/node_modules/@jinn-network/plugin/dist/pickup.d.ts +3 -12
  20. package/node_modules/@jinn-network/plugin/dist/pickup.js +9 -45
  21. package/node_modules/@jinn-network/plugin/dist/plugin.d.ts +1 -5
  22. package/node_modules/@jinn-network/plugin/dist/plugin.js +32 -121
  23. package/node_modules/@jinn-network/plugin/dist/ports/corpus-port.d.ts +0 -5
  24. package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-hit.d.ts +0 -2
  25. package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-hit.js +1 -9
  26. package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-packet.d.ts +4 -5
  27. package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-packet.js +7 -14
  28. package/node_modules/@jinn-network/plugin/dist/testing/in-memory-corpus.d.ts +1 -3
  29. package/node_modules/@jinn-network/plugin/dist/testing/in-memory-corpus.js +1 -5
  30. package/node_modules/@jinn-network/plugin/package.json +1 -1
  31. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "clientGitSha": "0b79cd5d1ec59cbf714a4d3814412d57fd7f7173",
2
+ "clientGitSha": "312473165a5618f5f3e45a0b94532b63788916e1",
3
3
  "codeDigest": "sha256:205201da01ad74e3474acce6b6c454c5e9aeab73ca0cba86124cd8547e057171",
4
- "builtAt": "2026-07-24T11:40:59.161Z",
5
- "implVersion": "0.2.0-canary.0b79cd5d"
4
+ "builtAt": "2026-07-24T09:04:59.282Z",
5
+ "implVersion": "0.2.0-canary.31247316"
6
6
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "commit": "0b79cd5d1ec59cbf714a4d3814412d57fd7f7173"
2
+ "commit": "312473165a5618f5f3e45a0b94532b63788916e1"
3
3
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinn-network/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.0",
4
4
  "description": "Domain core for Jinn — evidence/contribution stores, scrub, trajectory parsing, and corpus reads. Independent of client/src.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@huggingface/transformers": "^4.2.0",
29
- "@jinn-network/plugin": "0.1.1",
29
+ "@jinn-network/plugin": "0.1.0",
30
30
  "@lmoe/gliner-onnx": "0.1.0",
31
31
  "@noble/hashes": "^2.2.0",
32
32
  "@secretlint/core": "^13.0.2",
@@ -19,7 +19,7 @@ export type { ContributionPort, ContributionLedgerEntry, ContributionLocalState,
19
19
  export type { LocalLearningPort, LocalLearningRun, LocalLearningSkill, } from './ports/local-learning-port.js';
20
20
  export type { SkillsPort, SkillRecord } from './ports/skills-port.js';
21
21
  export { createJinnPlugin, JINN_PLUGIN_CONTRACT_VERSION, PluginSession } from './plugin.js';
22
- export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupOptions, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
22
+ export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
23
23
  export { PickupConfigSchema, DEFAULT_PICKUP_CONFIG, parsePickupConfig, TIER_ORDER } from './schemas/pickup-config.js';
24
24
  export type { PickupConfig, Tier } from './schemas/pickup-config.js';
25
25
  export { deriveRepositorySearchTerms, discriminatingTerms, deriveSearchTerms, classifyPayload, dedupeKnowledgeHits, scoreKnowledgeHit, selectKnowledgeHits, rankKnowledgeHits, MAX_SELECTED_PACKETS, } from './pickup.js';
@@ -62,15 +62,7 @@ export declare function scoreKnowledgeHit(hit: KnowledgeHit, terms: string[]): n
62
62
  * cannot manufacture relevance.
63
63
  */
64
64
  export declare function scoreKnowledgeRecord(hit: KnowledgeHit, record: CorpusRecord, terms: string[]): number;
65
- /**
66
- * Return a copied, deterministically ranked scored-candidate list.
67
- *
68
- * Recency is a group property, not a pairwise comparator: within each equal
69
- * score+tier group, use recency only when every hit declares the same domain
70
- * (including the all-omitted legacy group). A mixed-domain group preserves
71
- * its stable score/tier input order rather than comparing incomparable
72
- * recency values or inventing a source priority.
73
- */
65
+ /** Return a copied, deterministically ranked scored-candidate list. */
74
66
  export declare function rankScoredKnowledgeHits(candidates: ScoredKnowledgeHit[]): ScoredKnowledgeHit[];
75
67
  /**
76
68
  * Eligible, deduplicated candidates that have at least one metadata match.
@@ -81,9 +73,8 @@ export declare function rankKnowledgeCandidates(hits: KnowledgeHit[], terms: str
81
73
  /**
82
74
  * Full ranked candidate pool (rescope §3.3 selection policy): drop skill
83
75
  * hits, dedup, score, apply the relevance floor (honest nothing-found below
84
- * it), rank score desc → tier desc → comparable-domain recency desc (or ref
85
- * for a mixed-domain tie group) — every candidate that clears the floor, not
86
- * sliced to `MAX_SELECTED_PACKETS`.
76
+ * it), rank score desc → tier desc → recency desc — every candidate that
77
+ * clears the floor, not sliced to `MAX_SELECTED_PACKETS`.
87
78
  *
88
79
  * Content-level guards that can only run after a candidate's content is
89
80
  * fetched (mono #1782: post-fetch skill-payload classification, empty-packet
@@ -293,52 +293,17 @@ function tierRank(tier) {
293
293
  return -1;
294
294
  return TIER_ORDER.indexOf(tier);
295
295
  }
296
- function compareScoreAndTier(a, b) {
296
+ function compareScoredKnowledgeHits(a, b) {
297
297
  if (b.score !== a.score)
298
298
  return b.score - a.score;
299
- return tierRank(b.hit.tier) - tierRank(a.hit.tier);
299
+ const tierDiff = tierRank(b.hit.tier) - tierRank(a.hit.tier);
300
+ if (tierDiff !== 0)
301
+ return tierDiff;
302
+ return (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
300
303
  }
301
- function compareRef(a, b) {
302
- if (a.hit.ref < b.hit.ref)
303
- return -1;
304
- if (a.hit.ref > b.hit.ref)
305
- return 1;
306
- return 0;
307
- }
308
- /**
309
- * Return a copied, deterministically ranked scored-candidate list.
310
- *
311
- * Recency is a group property, not a pairwise comparator: within each equal
312
- * score+tier group, use recency only when every hit declares the same domain
313
- * (including the all-omitted legacy group). A mixed-domain group preserves
314
- * its stable score/tier input order rather than comparing incomparable
315
- * recency values or inventing a source priority.
316
- */
304
+ /** Return a copied, deterministically ranked scored-candidate list. */
317
305
  export function rankScoredKnowledgeHits(candidates) {
318
- const grouped = [...candidates].sort(compareScoreAndTier);
319
- const ranked = [];
320
- let start = 0;
321
- while (start < grouped.length) {
322
- let end = start + 1;
323
- while (end < grouped.length
324
- && compareScoreAndTier(grouped[start], grouped[end]) === 0) {
325
- end += 1;
326
- }
327
- const tieGroup = grouped.slice(start, end);
328
- const recencyDomain = tieGroup[0].hit.recencyDomain;
329
- const comparableRecency = tieGroup.every((candidate) => candidate.hit.recencyDomain === recencyDomain);
330
- if (comparableRecency) {
331
- tieGroup.sort((a, b) => {
332
- const recencyDiff = (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
333
- if (recencyDiff !== 0)
334
- return recencyDiff;
335
- return compareRef(a, b);
336
- });
337
- }
338
- ranked.push(...tieGroup);
339
- start = end;
340
- }
341
- return ranked;
306
+ return [...candidates].sort(compareScoredKnowledgeHits);
342
307
  }
343
308
  /**
344
309
  * Eligible, deduplicated candidates that have at least one metadata match.
@@ -355,9 +320,8 @@ export function rankKnowledgeCandidates(hits, terms) {
355
320
  /**
356
321
  * Full ranked candidate pool (rescope §3.3 selection policy): drop skill
357
322
  * hits, dedup, score, apply the relevance floor (honest nothing-found below
358
- * it), rank score desc → tier desc → comparable-domain recency desc (or ref
359
- * for a mixed-domain tie group) — every candidate that clears the floor, not
360
- * sliced to `MAX_SELECTED_PACKETS`.
323
+ * it), rank score desc → tier desc → recency desc — every candidate that
324
+ * clears the floor, not sliced to `MAX_SELECTED_PACKETS`.
361
325
  *
362
326
  * Content-level guards that can only run after a candidate's content is
363
327
  * fetched (mono #1782: post-fetch skill-payload classification, empty-packet
@@ -50,14 +50,10 @@ export interface FirstTurnPickupResult {
50
50
  retrievalFired: boolean;
51
51
  eligibleRefs: string[];
52
52
  deliveredRefs: string[];
53
- deliveredCanonicalEpisodeIds: string[];
54
53
  deliveryMode: 'delivered' | 'disabled' | 'degraded' | 'withheld';
55
54
  deliveredContentHash?: string;
56
55
  degraded?: string;
57
56
  }
58
- export interface FirstTurnPickupOptions {
59
- excludeCanonicalEpisodeIds?: readonly string[];
60
- }
61
57
  export interface ToolCallEvent {
62
58
  spanId: string;
63
59
  parentSpanId: string | null;
@@ -140,7 +136,7 @@ export declare class PluginSession {
140
136
  private deliveredContentHash;
141
137
  private readonly capturedAt;
142
138
  constructor(deps: JinnPluginDeps, meta: SessionMeta);
143
- firstTurnPickup(firstMessage: string, options?: FirstTurnPickupOptions): Promise<FirstTurnPickupResult>;
139
+ firstTurnPickup(firstMessage: string): Promise<FirstTurnPickupResult>;
144
140
  noteUserTurn(content: string): void;
145
141
  noteAssistantTurn(content: string): void;
146
142
  private pushTurn;
@@ -227,8 +227,7 @@ export class PluginSession {
227
227
  this.deps = deps;
228
228
  this.meta = meta;
229
229
  }
230
- async firstTurnPickup(firstMessage, options = {}) {
231
- const excludedCanonicalEpisodeIds = new Set(options.excludeCanonicalEpisodeIds ?? []);
230
+ async firstTurnPickup(firstMessage) {
232
231
  const config = parsePickupConfig(this.meta.pickup);
233
232
  if (!config.enabled) {
234
233
  return {
@@ -238,7 +237,6 @@ export class PluginSession {
238
237
  retrievalFired: false,
239
238
  eligibleRefs: [],
240
239
  deliveredRefs: [],
241
- deliveredCanonicalEpisodeIds: [],
242
240
  deliveryMode: 'disabled',
243
241
  };
244
242
  }
@@ -259,7 +257,6 @@ export class PluginSession {
259
257
  retrievalFired: true,
260
258
  eligibleRefs: [],
261
259
  deliveredRefs: [],
262
- deliveredCanonicalEpisodeIds: [],
263
260
  deliveryMode: this.deliveryMode,
264
261
  };
265
262
  }
@@ -286,19 +283,7 @@ export class PluginSession {
286
283
  byRef.set(hit.ref, hit);
287
284
  }
288
285
  }
289
- // Only the production local search adapter exposes this hit-level hint in
290
- // this release. Federated search is stable local-then-public, so the first
291
- // hit for an identity is the deterministic local form to prefer once a
292
- // fetched public record reveals that same identity.
293
- const preferredHitByCanonicalEpisodeId = new Map();
294
- for (const hit of byRef.values()) {
295
- if (hit.canonicalEpisodeId !== undefined
296
- && !preferredHitByCanonicalEpisodeId.has(hit.canonicalEpisodeId)) {
297
- preferredHitByCanonicalEpisodeId.set(hit.canonicalEpisodeId, hit);
298
- }
299
- }
300
- const candidates = rankKnowledgeCandidates([...byRef.values()].filter((hit) => hit.canonicalEpisodeId === undefined
301
- || !excludedCanonicalEpisodeIds.has(hit.canonicalEpisodeId)), scoringTerms);
286
+ const candidates = rankKnowledgeCandidates([...byRef.values()], scoringTerms);
302
287
  if (candidates.length === 0) {
303
288
  this.deliveryMode = degradedReason === undefined ? 'withheld' : 'degraded';
304
289
  return {
@@ -308,7 +293,6 @@ export class PluginSession {
308
293
  retrievalFired: true,
309
294
  eligibleRefs: [],
310
295
  deliveredRefs: [],
311
- deliveredCanonicalEpisodeIds: [],
312
296
  deliveryMode: this.deliveryMode,
313
297
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
314
298
  };
@@ -323,78 +307,25 @@ export class PluginSession {
323
307
  // floor and records an honest degraded reason (fail open).
324
308
  const fetchedRefs = [];
325
309
  const prefetchedByRef = new Map();
326
- const fetchRecord = (hit) => {
327
- const prefetched = prefetchedByRef.get(hit.ref);
328
- if (prefetched !== undefined)
329
- return prefetched;
330
- fetchedRefs.push(hit.ref);
331
- const pending = this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
332
- prefetchedByRef.set(hit.ref, pending);
333
- return pending;
334
- };
335
- const resolvePreferredRecord = async (hit, fetchedRecord) => {
336
- let selectedHit = hit;
337
- let record = fetchedRecord;
338
- let preferredFailureReason;
339
- const canonicalEpisodeId = record.canonicalEpisodeId;
340
- const preferredHit = canonicalEpisodeId === undefined
341
- ? undefined
342
- : preferredHitByCanonicalEpisodeId.get(canonicalEpisodeId);
343
- if (preferredHit !== undefined && preferredHit.ref !== hit.ref) {
344
- const preferredResult = await fetchRecord(preferredHit);
345
- if (preferredResult.status === 'unavailable') {
346
- preferredFailureReason = preferredResult.reason;
347
- }
348
- else {
349
- if (preferredResult.status === 'degraded') {
350
- preferredFailureReason = preferredResult.reason;
351
- }
352
- const preferredRecord = valueOr(preferredResult, null);
353
- if (preferredRecord !== null) {
354
- selectedHit = preferredHit;
355
- record = preferredRecord;
356
- }
357
- }
358
- }
359
- return {
360
- selectedHit,
361
- record,
362
- ...(preferredFailureReason !== undefined ? { preferredFailureReason } : {}),
363
- };
364
- };
365
310
  const directCandidates = candidates.filter((candidate) => candidate.score >= RELEVANCE_FLOOR);
366
311
  const nearMisses = candidates
367
312
  .filter((candidate) => candidate.score < RELEVANCE_FLOOR)
368
313
  .slice(0, MAX_CONTENT_RESCORE_CANDIDATES);
369
- const nearMissResults = await Promise.all(nearMisses.map(async ({ hit }) => {
370
- const result = await fetchRecord(hit);
371
- if (result.status === 'unavailable') {
372
- return { result, selectedHit: hit, record: null };
373
- }
374
- const fetchedRecord = valueOr(result, null);
375
- if (fetchedRecord === null)
376
- return { result, selectedHit: hit, record: null };
377
- if (fetchedRecord.canonicalEpisodeId !== undefined
378
- && excludedCanonicalEpisodeIds.has(fetchedRecord.canonicalEpisodeId)) {
379
- return { result, selectedHit: hit, record: null };
380
- }
381
- const preferred = await resolvePreferredRecord(hit, fetchedRecord);
382
- return { result, ...preferred };
314
+ const nearMissResults = await Promise.all(nearMisses.map(({ hit }) => {
315
+ fetchedRefs.push(hit.ref);
316
+ return this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
383
317
  }));
384
318
  const promotedCandidates = [];
385
319
  for (let index = 0; index < nearMisses.length; index += 1) {
386
320
  const candidate = nearMisses[index];
387
- const nearMissResult = nearMissResults[index];
388
- if (nearMissResult.result.status === 'unavailable') {
321
+ const result = nearMissResults[index];
322
+ prefetchedByRef.set(candidate.hit.ref, result);
323
+ if (result.status !== 'ok') {
389
324
  if (degradedReason === undefined)
390
- degradedReason = nearMissResult.result.reason;
325
+ degradedReason = result.reason;
391
326
  continue;
392
327
  }
393
- if (nearMissResult.result.status === 'degraded') {
394
- degradedReason ??= nearMissResult.result.reason;
395
- }
396
- degradedReason ??= nearMissResult.preferredFailureReason;
397
- const record = nearMissResult.record;
328
+ const record = result.value;
398
329
  if (record === null)
399
330
  continue;
400
331
  if (record.isSkillPayload === true)
@@ -419,55 +350,40 @@ export class PluginSession {
419
350
  retrievalFired: true,
420
351
  eligibleRefs: [],
421
352
  deliveredRefs: [],
422
- deliveredCanonicalEpisodeIds: [],
423
353
  deliveryMode: this.deliveryMode,
424
354
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
425
355
  };
426
356
  }
427
357
  // Fetch full content for ranked candidates and project packets, walking
428
358
  // down the ranked list until MAX_SELECTED_PACKETS valid packets are
429
- // found or candidates are exhausted (mono #1782). Post-fetch guards can
430
- // disqualify a candidate without spending its slot, promoting the
431
- // next-ranked one: (1) canonical exclusion/dedup; (2) content-level
432
- // skill classification — which excludes a legacy skill-shaped record
433
- // (skill.md step attribute) or a jinn.skill.v1-backed record that slipped
434
- // the wire kind filter, exactly as a wire kind:'skill' hit is excluded at
435
- // selection time; (3) retrieval-visibility content verification (#1824,
436
- // W2) — fail-closed where the other guards are fail-open; (4)
437
- // empty-packet honesty — a projection with zero excerpts and no synthesis
438
- // is not evidence. A projection failure degrades that one ref to
439
- // nothing-found rather than throwing into the caller (§3.5).
359
+ // found or candidates are exhausted (mono #1782). Three post-fetch
360
+ // guards can disqualify a candidate without spending its slot, promoting
361
+ // the next-ranked one: (1) content-level skill classification — excludes
362
+ // a legacy skill-shaped record (skill.md step attribute) or a
363
+ // jinn.skill.v1-backed record that slipped the wire kind filter, exactly
364
+ // as a wire kind:'skill' hit is excluded at selection time; (2)
365
+ // retrieval-visibility content verification (#1824, W2) — fail-closed
366
+ // where the other two are fail-open; (3) empty-packet honesty — a
367
+ // projection with zero excerpts and no synthesis is not evidence. A
368
+ // projection failure degrades that one ref to nothing-found rather than
369
+ // throwing into the caller (§3.5).
440
370
  const packets = [];
441
- const deliveredCanonicalEpisodeIds = [];
442
- const deliveredCanonicalSet = new Set();
443
371
  for (const hit of ranked) {
444
372
  if (packets.length >= MAX_SELECTED_PACKETS)
445
373
  break;
446
- const result = await fetchRecord(hit);
447
- if (result.status === 'unavailable') {
374
+ let result = prefetchedByRef.get(hit.ref);
375
+ if (result === undefined) {
376
+ fetchedRefs.push(hit.ref);
377
+ result = await this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
378
+ }
379
+ if (result.status !== 'ok') {
448
380
  if (degradedReason === undefined)
449
381
  degradedReason = result.reason;
450
382
  continue;
451
383
  }
452
- if (result.status === 'degraded')
453
- degradedReason ??= result.reason;
454
- const fetchedRecord = valueOr(result, null);
455
- if (fetchedRecord === null)
456
- continue;
457
- const fetchedCanonicalEpisodeId = fetchedRecord.canonicalEpisodeId;
458
- if (fetchedCanonicalEpisodeId !== undefined
459
- && (excludedCanonicalEpisodeIds.has(fetchedCanonicalEpisodeId)
460
- || deliveredCanonicalSet.has(fetchedCanonicalEpisodeId))) {
461
- continue;
462
- }
463
- const { selectedHit, record, preferredFailureReason, } = await resolvePreferredRecord(hit, fetchedRecord);
464
- degradedReason ??= preferredFailureReason;
465
- const canonicalEpisodeId = record.canonicalEpisodeId;
466
- if (canonicalEpisodeId !== undefined
467
- && (excludedCanonicalEpisodeIds.has(canonicalEpisodeId)
468
- || deliveredCanonicalSet.has(canonicalEpisodeId))) {
384
+ const record = result.value;
385
+ if (record === null)
469
386
  continue;
470
- }
471
387
  if (record.isSkillPayload === true)
472
388
  continue;
473
389
  // Post-fetch content guard (#1824, W2): content is the truth, the
@@ -481,16 +397,12 @@ export class PluginSession {
481
397
  packet = projectKnowledgePacket(record);
482
398
  }
483
399
  catch (error) {
484
- degradedReason ??= `packet projection failed for ${selectedHit.ref}: ${errorReason(error)}`;
400
+ degradedReason ??= `packet projection failed for ${hit.ref}: ${errorReason(error)}`;
485
401
  continue;
486
402
  }
487
403
  if (packet.excerpts.length === 0 && packet.synthesis === undefined)
488
404
  continue;
489
405
  packets.push(packet);
490
- if (canonicalEpisodeId !== undefined) {
491
- deliveredCanonicalSet.add(canonicalEpisodeId);
492
- deliveredCanonicalEpisodeIds.push(canonicalEpisodeId);
493
- }
494
406
  }
495
407
  this.fetchedRefs = fetchedRefs;
496
408
  this.providedRefs = packets.map((packet) => packet.ref);
@@ -505,7 +417,6 @@ export class PluginSession {
505
417
  retrievalFired: true,
506
418
  eligibleRefs: this.eligibleRefs,
507
419
  deliveredRefs: [],
508
- deliveredCanonicalEpisodeIds: [],
509
420
  deliveryMode: this.deliveryMode,
510
421
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
511
422
  };
@@ -520,7 +431,6 @@ export class PluginSession {
520
431
  retrievalFired: true,
521
432
  eligibleRefs: this.eligibleRefs,
522
433
  deliveredRefs: this.providedRefs,
523
- deliveredCanonicalEpisodeIds,
524
434
  deliveryMode: this.deliveryMode,
525
435
  deliveredContentHash: this.deliveredContentHash,
526
436
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
@@ -626,7 +536,8 @@ function renderKnowledgePacket(packet) {
626
536
  for (const excerpt of packet.excerpts)
627
537
  lines.push(`- ${excerpt.label}: ${excerpt.text}`);
628
538
  const capturedDate = packet.attribution.capturedAt.slice(0, 10);
629
- lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate}`);
539
+ lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate} · `
540
+ + `full episode: corpus_fetch ${packet.ref}`);
630
541
  return lines.join('\n');
631
542
  }
632
543
  function renderKnowledgePackets(packets) {
@@ -19,11 +19,6 @@ export interface CorpusRecordStep {
19
19
  */
20
20
  export interface CorpusRecord {
21
21
  ref: string;
22
- /**
23
- * Canonical EpisodeV1 episodeId, or the legacy trace sessionId used as its
24
- * read-compatible identity. Application metadata for deduplication only.
25
- */
26
- canonicalEpisodeId?: string;
27
22
  task: {
28
23
  summary: string;
29
24
  repositorySlug?: string;
@@ -2,7 +2,6 @@
2
2
  import { z } from 'zod';
3
3
  export declare const KnowledgeHitSchema: z.ZodObject<{
4
4
  ref: z.ZodString;
5
- canonicalEpisodeId: z.ZodOptional<z.ZodString>;
6
5
  kind: z.ZodEnum<{
7
6
  seed: "seed";
8
7
  trace: "trace";
@@ -23,7 +22,6 @@ export declare const KnowledgeHitSchema: z.ZodObject<{
23
22
  tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
23
  origin: z.ZodOptional<z.ZodString>;
25
24
  publishedAt: z.ZodOptional<z.ZodNumber>;
26
- recencyDomain: z.ZodOptional<z.ZodString>;
27
25
  retrievalVisible: z.ZodOptional<z.ZodBoolean>;
28
26
  }, z.core.$strict>;
29
27
  export type KnowledgeHit = z.infer<typeof KnowledgeHitSchema>;
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
  import { TIER_ORDER } from './pickup-config.js';
4
4
  export const KnowledgeHitSchema = z.strictObject({
5
5
  ref: z.string().min(1),
6
- canonicalEpisodeId: z.string().min(1).optional(),
7
6
  kind: z.enum(['seed', 'trace', 'skill']),
8
7
  title: z.string().min(1).optional(),
9
8
  snippet: z.string().min(1).optional(),
@@ -15,15 +14,8 @@ export const KnowledgeHitSchema = z.strictObject({
15
14
  /** Trustworthy content-dedup identity: on-chain agentId when known, otherwise
16
15
  * the record ref. Never a manifest-supplied safeAddress. */
17
16
  origin: z.string().min(1).optional(),
18
- /** Adapter-native recency value — comparable only within `recencyDomain`. */
17
+ /** Unix-ms publish time — the selection policy's recency tiebreaker (rescope §3.3). */
19
18
  publishedAt: z.number().int().nonnegative().optional(),
20
- /**
21
- * Adapter-declared comparison domain for `publishedAt` (for example,
22
- * `unix-ms` or `block-number`). Recency is comparable only within one
23
- * explicit domain. When both hits omit this additive field, ranking keeps
24
- * the legacy raw-number behavior.
25
- */
26
- recencyDomain: z.string().min(1).optional(),
27
19
  /** Computed by the adapter from the wire hit's tags (issue #1824) — presence
28
20
  * of RETRIEVAL_VISIBLE_TAG. Absent = treat as not visible (fail-closed at
29
21
  * ranking). */
@@ -65,12 +65,11 @@ export interface KnowledgePacketBudget {
65
65
  maxChars?: number;
66
66
  }
67
67
  /**
68
- * Line-boundary-aware truncation ending with a neutral marker. The `ref`
69
- * parameter remains required for source compatibility with the public helper.
70
- * Returns an empty string when the budget cannot fit both meaningful source
71
- * text and the complete tail.
68
+ * Line-boundary-aware truncation ending with an explicit, pointer-bearing
69
+ * tail. Returns an empty string when the budget cannot fit both meaningful
70
+ * source text and the complete tail.
72
71
  */
73
- export declare function truncateLineBoundary(text: string, maxChars: number, _ref: string): string;
72
+ export declare function truncateLineBoundary(text: string, maxChars: number, ref: string): string;
74
73
  /**
75
74
  * Pure deterministic projection of a `CorpusRecord` into a `KnowledgePacket`
76
75
  * (rescope §3.2). Selects and truncates; never paraphrases. Enforces the
@@ -60,9 +60,6 @@ function stepDiff(step) {
60
60
  function stepNote(step) {
61
61
  return attrString(step.attributes, 'note') ?? attrString(step.attributes, 'turn.text');
62
62
  }
63
- function isAssistantTurn(step) {
64
- return attrString(step.attributes, 'role') === 'assistant';
65
- }
66
63
  const EXCERPT_LABELS = new Set([
67
64
  'failure', 'fix', 'command', 'diff', 'note',
68
65
  ]);
@@ -93,9 +90,7 @@ function seedStepExcerpt(step) {
93
90
  * output, the next command after it that is not itself a failure (the
94
91
  * correction), the last passing command overall (the final backstop), and a
95
92
  * diff step when present. Falls back to a single free-form note when
96
- * nothing command-shaped is found, preferring an assistant turn so a
97
- * conversational episode supplies the retained answer rather than merely
98
- * repeating the user's prompt. Selects only — never paraphrases.
93
+ * nothing command-shaped is found. Selects only — never paraphrases.
99
94
  */
100
95
  function selectExcerpts(steps) {
101
96
  const seedExcerpts = steps
@@ -140,23 +135,21 @@ function selectExcerpts(steps) {
140
135
  if (diffStep !== undefined)
141
136
  excerpts.push({ label: 'diff', text: stepDiff(diffStep) });
142
137
  if (excerpts.length === 0) {
143
- const noteStep = steps.find((step) => isAssistantTurn(step) && stepNote(step) !== undefined)
144
- ?? steps.find((step) => stepNote(step) !== undefined);
138
+ const noteStep = steps.find((step) => stepNote(step) !== undefined);
145
139
  if (noteStep !== undefined)
146
140
  excerpts.push({ label: 'note', text: stepNote(noteStep) });
147
141
  }
148
142
  return excerpts;
149
143
  }
150
144
  /**
151
- * Line-boundary-aware truncation ending with a neutral marker. The `ref`
152
- * parameter remains required for source compatibility with the public helper.
153
- * Returns an empty string when the budget cannot fit both meaningful source
154
- * text and the complete tail.
145
+ * Line-boundary-aware truncation ending with an explicit, pointer-bearing
146
+ * tail. Returns an empty string when the budget cannot fit both meaningful
147
+ * source text and the complete tail.
155
148
  */
156
- export function truncateLineBoundary(text, maxChars, _ref) {
149
+ export function truncateLineBoundary(text, maxChars, ref) {
157
150
  if (text.length <= maxChars)
158
151
  return text;
159
- const tail = '\n[truncated]';
152
+ const tail = `\n[truncated — full episode: corpus_fetch ${ref}]`;
160
153
  const budget = maxChars - tail.length;
161
154
  if (budget <= 0)
162
155
  return '';
@@ -6,7 +6,7 @@ import type { KnowledgeHit } from '../schemas/knowledge-hit.js';
6
6
  * search would return for it (kind/title/tier/payloadKind/publishedAt — the
7
7
  * fields not derivable from `CorpusRecord` alone).
8
8
  */
9
- export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt' | 'recencyDomain'>>;
9
+ export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt'>>;
10
10
  /** Map/array-backed, seedable — architecture spec §8. Seeds carry full
11
11
  * content (`CorpusRecord`) so `get()` can return it directly; `search()`
12
12
  * derives the lightweight `KnowledgeHit` view. */
@@ -17,7 +17,6 @@ export declare class InMemoryCorpusPort implements CorpusPort {
17
17
  ref: string;
18
18
  kind: "seed" | "trace" | "skill";
19
19
  tags: string[];
20
- canonicalEpisodeId?: string | undefined;
21
20
  title?: string | undefined;
22
21
  snippet?: string | undefined;
23
22
  score?: number | undefined;
@@ -25,7 +24,6 @@ export declare class InMemoryCorpusPort implements CorpusPort {
25
24
  payloadKind?: "unknown" | "skill" | undefined;
26
25
  origin?: string | undefined;
27
26
  publishedAt?: number | undefined;
28
- recencyDomain?: string | undefined;
29
27
  retrievalVisible?: boolean | undefined;
30
28
  }[]>>;
31
29
  get(ref: string): Promise<{
@@ -2,9 +2,6 @@ import { ok } from '../outcome.js';
2
2
  function toKnowledgeHit(seed) {
3
3
  return {
4
4
  ref: seed.ref,
5
- ...(seed.canonicalEpisodeId !== undefined
6
- ? { canonicalEpisodeId: seed.canonicalEpisodeId }
7
- : {}),
8
5
  kind: seed.kind,
9
6
  ...(seed.title !== undefined ? { title: seed.title } : {}),
10
7
  snippet: seed.task.summary,
@@ -13,7 +10,6 @@ function toKnowledgeHit(seed) {
13
10
  tags: seed.tags,
14
11
  origin: seed.origin,
15
12
  ...(seed.publishedAt !== undefined ? { publishedAt: seed.publishedAt } : {}),
16
- ...(seed.recencyDomain !== undefined ? { recencyDomain: seed.recencyDomain } : {}),
17
13
  retrievalVisible: seed.retrievalVisible,
18
14
  };
19
15
  }
@@ -52,7 +48,7 @@ export class InMemoryCorpusPort {
52
48
  if (!record)
53
49
  return ok(null);
54
50
  // Strip the KnowledgeHit-only fields — get() returns the CorpusRecord shape.
55
- const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, recencyDomain: _recencyDomain, ...corpusRecord } = record;
51
+ const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, ...corpusRecord } = record;
56
52
  return ok(corpusRecord);
57
53
  }
58
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinn-network/plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.0",
4
4
  "description": "Product core for the Jinn Plugin — ports, product schemas, and the createJinnPlugin factory. Stage 1 foundation package (contracts + in-memory kit only).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinn-network/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.0",
4
4
  "description": "Domain core for Jinn — evidence/contribution stores, scrub, trajectory parsing, and corpus reads. Independent of client/src.",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.13.0",
@@ -19,7 +19,7 @@ export type { ContributionPort, ContributionLedgerEntry, ContributionLocalState,
19
19
  export type { LocalLearningPort, LocalLearningRun, LocalLearningSkill, } from './ports/local-learning-port.js';
20
20
  export type { SkillsPort, SkillRecord } from './ports/skills-port.js';
21
21
  export { createJinnPlugin, JINN_PLUGIN_CONTRACT_VERSION, PluginSession } from './plugin.js';
22
- export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupOptions, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
22
+ export type { CompleteSessionEligibilityInputs, CompleteSessionInput, JinnPlugin, JinnPluginDeps, SessionMeta, FirstTurnPickupResult, ToolCallEvent, SessionOutcome, SessionEndResult, ContributionCompletionReceipt, ContributionPreview, } from './plugin.js';
23
23
  export { PickupConfigSchema, DEFAULT_PICKUP_CONFIG, parsePickupConfig, TIER_ORDER } from './schemas/pickup-config.js';
24
24
  export type { PickupConfig, Tier } from './schemas/pickup-config.js';
25
25
  export { deriveRepositorySearchTerms, discriminatingTerms, deriveSearchTerms, classifyPayload, dedupeKnowledgeHits, scoreKnowledgeHit, selectKnowledgeHits, rankKnowledgeHits, MAX_SELECTED_PACKETS, } from './pickup.js';
@@ -62,15 +62,7 @@ export declare function scoreKnowledgeHit(hit: KnowledgeHit, terms: string[]): n
62
62
  * cannot manufacture relevance.
63
63
  */
64
64
  export declare function scoreKnowledgeRecord(hit: KnowledgeHit, record: CorpusRecord, terms: string[]): number;
65
- /**
66
- * Return a copied, deterministically ranked scored-candidate list.
67
- *
68
- * Recency is a group property, not a pairwise comparator: within each equal
69
- * score+tier group, use recency only when every hit declares the same domain
70
- * (including the all-omitted legacy group). A mixed-domain group preserves
71
- * its stable score/tier input order rather than comparing incomparable
72
- * recency values or inventing a source priority.
73
- */
65
+ /** Return a copied, deterministically ranked scored-candidate list. */
74
66
  export declare function rankScoredKnowledgeHits(candidates: ScoredKnowledgeHit[]): ScoredKnowledgeHit[];
75
67
  /**
76
68
  * Eligible, deduplicated candidates that have at least one metadata match.
@@ -81,9 +73,8 @@ export declare function rankKnowledgeCandidates(hits: KnowledgeHit[], terms: str
81
73
  /**
82
74
  * Full ranked candidate pool (rescope §3.3 selection policy): drop skill
83
75
  * hits, dedup, score, apply the relevance floor (honest nothing-found below
84
- * it), rank score desc → tier desc → comparable-domain recency desc (or ref
85
- * for a mixed-domain tie group) — every candidate that clears the floor, not
86
- * sliced to `MAX_SELECTED_PACKETS`.
76
+ * it), rank score desc → tier desc → recency desc — every candidate that
77
+ * clears the floor, not sliced to `MAX_SELECTED_PACKETS`.
87
78
  *
88
79
  * Content-level guards that can only run after a candidate's content is
89
80
  * fetched (mono #1782: post-fetch skill-payload classification, empty-packet
@@ -293,52 +293,17 @@ function tierRank(tier) {
293
293
  return -1;
294
294
  return TIER_ORDER.indexOf(tier);
295
295
  }
296
- function compareScoreAndTier(a, b) {
296
+ function compareScoredKnowledgeHits(a, b) {
297
297
  if (b.score !== a.score)
298
298
  return b.score - a.score;
299
- return tierRank(b.hit.tier) - tierRank(a.hit.tier);
299
+ const tierDiff = tierRank(b.hit.tier) - tierRank(a.hit.tier);
300
+ if (tierDiff !== 0)
301
+ return tierDiff;
302
+ return (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
300
303
  }
301
- function compareRef(a, b) {
302
- if (a.hit.ref < b.hit.ref)
303
- return -1;
304
- if (a.hit.ref > b.hit.ref)
305
- return 1;
306
- return 0;
307
- }
308
- /**
309
- * Return a copied, deterministically ranked scored-candidate list.
310
- *
311
- * Recency is a group property, not a pairwise comparator: within each equal
312
- * score+tier group, use recency only when every hit declares the same domain
313
- * (including the all-omitted legacy group). A mixed-domain group preserves
314
- * its stable score/tier input order rather than comparing incomparable
315
- * recency values or inventing a source priority.
316
- */
304
+ /** Return a copied, deterministically ranked scored-candidate list. */
317
305
  export function rankScoredKnowledgeHits(candidates) {
318
- const grouped = [...candidates].sort(compareScoreAndTier);
319
- const ranked = [];
320
- let start = 0;
321
- while (start < grouped.length) {
322
- let end = start + 1;
323
- while (end < grouped.length
324
- && compareScoreAndTier(grouped[start], grouped[end]) === 0) {
325
- end += 1;
326
- }
327
- const tieGroup = grouped.slice(start, end);
328
- const recencyDomain = tieGroup[0].hit.recencyDomain;
329
- const comparableRecency = tieGroup.every((candidate) => candidate.hit.recencyDomain === recencyDomain);
330
- if (comparableRecency) {
331
- tieGroup.sort((a, b) => {
332
- const recencyDiff = (b.hit.publishedAt ?? 0) - (a.hit.publishedAt ?? 0);
333
- if (recencyDiff !== 0)
334
- return recencyDiff;
335
- return compareRef(a, b);
336
- });
337
- }
338
- ranked.push(...tieGroup);
339
- start = end;
340
- }
341
- return ranked;
306
+ return [...candidates].sort(compareScoredKnowledgeHits);
342
307
  }
343
308
  /**
344
309
  * Eligible, deduplicated candidates that have at least one metadata match.
@@ -355,9 +320,8 @@ export function rankKnowledgeCandidates(hits, terms) {
355
320
  /**
356
321
  * Full ranked candidate pool (rescope §3.3 selection policy): drop skill
357
322
  * hits, dedup, score, apply the relevance floor (honest nothing-found below
358
- * it), rank score desc → tier desc → comparable-domain recency desc (or ref
359
- * for a mixed-domain tie group) — every candidate that clears the floor, not
360
- * sliced to `MAX_SELECTED_PACKETS`.
323
+ * it), rank score desc → tier desc → recency desc — every candidate that
324
+ * clears the floor, not sliced to `MAX_SELECTED_PACKETS`.
361
325
  *
362
326
  * Content-level guards that can only run after a candidate's content is
363
327
  * fetched (mono #1782: post-fetch skill-payload classification, empty-packet
@@ -50,14 +50,10 @@ export interface FirstTurnPickupResult {
50
50
  retrievalFired: boolean;
51
51
  eligibleRefs: string[];
52
52
  deliveredRefs: string[];
53
- deliveredCanonicalEpisodeIds: string[];
54
53
  deliveryMode: 'delivered' | 'disabled' | 'degraded' | 'withheld';
55
54
  deliveredContentHash?: string;
56
55
  degraded?: string;
57
56
  }
58
- export interface FirstTurnPickupOptions {
59
- excludeCanonicalEpisodeIds?: readonly string[];
60
- }
61
57
  export interface ToolCallEvent {
62
58
  spanId: string;
63
59
  parentSpanId: string | null;
@@ -140,7 +136,7 @@ export declare class PluginSession {
140
136
  private deliveredContentHash;
141
137
  private readonly capturedAt;
142
138
  constructor(deps: JinnPluginDeps, meta: SessionMeta);
143
- firstTurnPickup(firstMessage: string, options?: FirstTurnPickupOptions): Promise<FirstTurnPickupResult>;
139
+ firstTurnPickup(firstMessage: string): Promise<FirstTurnPickupResult>;
144
140
  noteUserTurn(content: string): void;
145
141
  noteAssistantTurn(content: string): void;
146
142
  private pushTurn;
@@ -227,8 +227,7 @@ export class PluginSession {
227
227
  this.deps = deps;
228
228
  this.meta = meta;
229
229
  }
230
- async firstTurnPickup(firstMessage, options = {}) {
231
- const excludedCanonicalEpisodeIds = new Set(options.excludeCanonicalEpisodeIds ?? []);
230
+ async firstTurnPickup(firstMessage) {
232
231
  const config = parsePickupConfig(this.meta.pickup);
233
232
  if (!config.enabled) {
234
233
  return {
@@ -238,7 +237,6 @@ export class PluginSession {
238
237
  retrievalFired: false,
239
238
  eligibleRefs: [],
240
239
  deliveredRefs: [],
241
- deliveredCanonicalEpisodeIds: [],
242
240
  deliveryMode: 'disabled',
243
241
  };
244
242
  }
@@ -259,7 +257,6 @@ export class PluginSession {
259
257
  retrievalFired: true,
260
258
  eligibleRefs: [],
261
259
  deliveredRefs: [],
262
- deliveredCanonicalEpisodeIds: [],
263
260
  deliveryMode: this.deliveryMode,
264
261
  };
265
262
  }
@@ -286,19 +283,7 @@ export class PluginSession {
286
283
  byRef.set(hit.ref, hit);
287
284
  }
288
285
  }
289
- // Only the production local search adapter exposes this hit-level hint in
290
- // this release. Federated search is stable local-then-public, so the first
291
- // hit for an identity is the deterministic local form to prefer once a
292
- // fetched public record reveals that same identity.
293
- const preferredHitByCanonicalEpisodeId = new Map();
294
- for (const hit of byRef.values()) {
295
- if (hit.canonicalEpisodeId !== undefined
296
- && !preferredHitByCanonicalEpisodeId.has(hit.canonicalEpisodeId)) {
297
- preferredHitByCanonicalEpisodeId.set(hit.canonicalEpisodeId, hit);
298
- }
299
- }
300
- const candidates = rankKnowledgeCandidates([...byRef.values()].filter((hit) => hit.canonicalEpisodeId === undefined
301
- || !excludedCanonicalEpisodeIds.has(hit.canonicalEpisodeId)), scoringTerms);
286
+ const candidates = rankKnowledgeCandidates([...byRef.values()], scoringTerms);
302
287
  if (candidates.length === 0) {
303
288
  this.deliveryMode = degradedReason === undefined ? 'withheld' : 'degraded';
304
289
  return {
@@ -308,7 +293,6 @@ export class PluginSession {
308
293
  retrievalFired: true,
309
294
  eligibleRefs: [],
310
295
  deliveredRefs: [],
311
- deliveredCanonicalEpisodeIds: [],
312
296
  deliveryMode: this.deliveryMode,
313
297
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
314
298
  };
@@ -323,78 +307,25 @@ export class PluginSession {
323
307
  // floor and records an honest degraded reason (fail open).
324
308
  const fetchedRefs = [];
325
309
  const prefetchedByRef = new Map();
326
- const fetchRecord = (hit) => {
327
- const prefetched = prefetchedByRef.get(hit.ref);
328
- if (prefetched !== undefined)
329
- return prefetched;
330
- fetchedRefs.push(hit.ref);
331
- const pending = this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
332
- prefetchedByRef.set(hit.ref, pending);
333
- return pending;
334
- };
335
- const resolvePreferredRecord = async (hit, fetchedRecord) => {
336
- let selectedHit = hit;
337
- let record = fetchedRecord;
338
- let preferredFailureReason;
339
- const canonicalEpisodeId = record.canonicalEpisodeId;
340
- const preferredHit = canonicalEpisodeId === undefined
341
- ? undefined
342
- : preferredHitByCanonicalEpisodeId.get(canonicalEpisodeId);
343
- if (preferredHit !== undefined && preferredHit.ref !== hit.ref) {
344
- const preferredResult = await fetchRecord(preferredHit);
345
- if (preferredResult.status === 'unavailable') {
346
- preferredFailureReason = preferredResult.reason;
347
- }
348
- else {
349
- if (preferredResult.status === 'degraded') {
350
- preferredFailureReason = preferredResult.reason;
351
- }
352
- const preferredRecord = valueOr(preferredResult, null);
353
- if (preferredRecord !== null) {
354
- selectedHit = preferredHit;
355
- record = preferredRecord;
356
- }
357
- }
358
- }
359
- return {
360
- selectedHit,
361
- record,
362
- ...(preferredFailureReason !== undefined ? { preferredFailureReason } : {}),
363
- };
364
- };
365
310
  const directCandidates = candidates.filter((candidate) => candidate.score >= RELEVANCE_FLOOR);
366
311
  const nearMisses = candidates
367
312
  .filter((candidate) => candidate.score < RELEVANCE_FLOOR)
368
313
  .slice(0, MAX_CONTENT_RESCORE_CANDIDATES);
369
- const nearMissResults = await Promise.all(nearMisses.map(async ({ hit }) => {
370
- const result = await fetchRecord(hit);
371
- if (result.status === 'unavailable') {
372
- return { result, selectedHit: hit, record: null };
373
- }
374
- const fetchedRecord = valueOr(result, null);
375
- if (fetchedRecord === null)
376
- return { result, selectedHit: hit, record: null };
377
- if (fetchedRecord.canonicalEpisodeId !== undefined
378
- && excludedCanonicalEpisodeIds.has(fetchedRecord.canonicalEpisodeId)) {
379
- return { result, selectedHit: hit, record: null };
380
- }
381
- const preferred = await resolvePreferredRecord(hit, fetchedRecord);
382
- return { result, ...preferred };
314
+ const nearMissResults = await Promise.all(nearMisses.map(({ hit }) => {
315
+ fetchedRefs.push(hit.ref);
316
+ return this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
383
317
  }));
384
318
  const promotedCandidates = [];
385
319
  for (let index = 0; index < nearMisses.length; index += 1) {
386
320
  const candidate = nearMisses[index];
387
- const nearMissResult = nearMissResults[index];
388
- if (nearMissResult.result.status === 'unavailable') {
321
+ const result = nearMissResults[index];
322
+ prefetchedByRef.set(candidate.hit.ref, result);
323
+ if (result.status !== 'ok') {
389
324
  if (degradedReason === undefined)
390
- degradedReason = nearMissResult.result.reason;
325
+ degradedReason = result.reason;
391
326
  continue;
392
327
  }
393
- if (nearMissResult.result.status === 'degraded') {
394
- degradedReason ??= nearMissResult.result.reason;
395
- }
396
- degradedReason ??= nearMissResult.preferredFailureReason;
397
- const record = nearMissResult.record;
328
+ const record = result.value;
398
329
  if (record === null)
399
330
  continue;
400
331
  if (record.isSkillPayload === true)
@@ -419,55 +350,40 @@ export class PluginSession {
419
350
  retrievalFired: true,
420
351
  eligibleRefs: [],
421
352
  deliveredRefs: [],
422
- deliveredCanonicalEpisodeIds: [],
423
353
  deliveryMode: this.deliveryMode,
424
354
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
425
355
  };
426
356
  }
427
357
  // Fetch full content for ranked candidates and project packets, walking
428
358
  // down the ranked list until MAX_SELECTED_PACKETS valid packets are
429
- // found or candidates are exhausted (mono #1782). Post-fetch guards can
430
- // disqualify a candidate without spending its slot, promoting the
431
- // next-ranked one: (1) canonical exclusion/dedup; (2) content-level
432
- // skill classification — which excludes a legacy skill-shaped record
433
- // (skill.md step attribute) or a jinn.skill.v1-backed record that slipped
434
- // the wire kind filter, exactly as a wire kind:'skill' hit is excluded at
435
- // selection time; (3) retrieval-visibility content verification (#1824,
436
- // W2) — fail-closed where the other guards are fail-open; (4)
437
- // empty-packet honesty — a projection with zero excerpts and no synthesis
438
- // is not evidence. A projection failure degrades that one ref to
439
- // nothing-found rather than throwing into the caller (§3.5).
359
+ // found or candidates are exhausted (mono #1782). Three post-fetch
360
+ // guards can disqualify a candidate without spending its slot, promoting
361
+ // the next-ranked one: (1) content-level skill classification — excludes
362
+ // a legacy skill-shaped record (skill.md step attribute) or a
363
+ // jinn.skill.v1-backed record that slipped the wire kind filter, exactly
364
+ // as a wire kind:'skill' hit is excluded at selection time; (2)
365
+ // retrieval-visibility content verification (#1824, W2) — fail-closed
366
+ // where the other two are fail-open; (3) empty-packet honesty — a
367
+ // projection with zero excerpts and no synthesis is not evidence. A
368
+ // projection failure degrades that one ref to nothing-found rather than
369
+ // throwing into the caller (§3.5).
440
370
  const packets = [];
441
- const deliveredCanonicalEpisodeIds = [];
442
- const deliveredCanonicalSet = new Set();
443
371
  for (const hit of ranked) {
444
372
  if (packets.length >= MAX_SELECTED_PACKETS)
445
373
  break;
446
- const result = await fetchRecord(hit);
447
- if (result.status === 'unavailable') {
374
+ let result = prefetchedByRef.get(hit.ref);
375
+ if (result === undefined) {
376
+ fetchedRefs.push(hit.ref);
377
+ result = await this.deps.corpus.get(hit.ref).catch((error) => degraded(`corpus get rejected for ${hit.ref}: ${errorReason(error)}`));
378
+ }
379
+ if (result.status !== 'ok') {
448
380
  if (degradedReason === undefined)
449
381
  degradedReason = result.reason;
450
382
  continue;
451
383
  }
452
- if (result.status === 'degraded')
453
- degradedReason ??= result.reason;
454
- const fetchedRecord = valueOr(result, null);
455
- if (fetchedRecord === null)
456
- continue;
457
- const fetchedCanonicalEpisodeId = fetchedRecord.canonicalEpisodeId;
458
- if (fetchedCanonicalEpisodeId !== undefined
459
- && (excludedCanonicalEpisodeIds.has(fetchedCanonicalEpisodeId)
460
- || deliveredCanonicalSet.has(fetchedCanonicalEpisodeId))) {
461
- continue;
462
- }
463
- const { selectedHit, record, preferredFailureReason, } = await resolvePreferredRecord(hit, fetchedRecord);
464
- degradedReason ??= preferredFailureReason;
465
- const canonicalEpisodeId = record.canonicalEpisodeId;
466
- if (canonicalEpisodeId !== undefined
467
- && (excludedCanonicalEpisodeIds.has(canonicalEpisodeId)
468
- || deliveredCanonicalSet.has(canonicalEpisodeId))) {
384
+ const record = result.value;
385
+ if (record === null)
469
386
  continue;
470
- }
471
387
  if (record.isSkillPayload === true)
472
388
  continue;
473
389
  // Post-fetch content guard (#1824, W2): content is the truth, the
@@ -481,16 +397,12 @@ export class PluginSession {
481
397
  packet = projectKnowledgePacket(record);
482
398
  }
483
399
  catch (error) {
484
- degradedReason ??= `packet projection failed for ${selectedHit.ref}: ${errorReason(error)}`;
400
+ degradedReason ??= `packet projection failed for ${hit.ref}: ${errorReason(error)}`;
485
401
  continue;
486
402
  }
487
403
  if (packet.excerpts.length === 0 && packet.synthesis === undefined)
488
404
  continue;
489
405
  packets.push(packet);
490
- if (canonicalEpisodeId !== undefined) {
491
- deliveredCanonicalSet.add(canonicalEpisodeId);
492
- deliveredCanonicalEpisodeIds.push(canonicalEpisodeId);
493
- }
494
406
  }
495
407
  this.fetchedRefs = fetchedRefs;
496
408
  this.providedRefs = packets.map((packet) => packet.ref);
@@ -505,7 +417,6 @@ export class PluginSession {
505
417
  retrievalFired: true,
506
418
  eligibleRefs: this.eligibleRefs,
507
419
  deliveredRefs: [],
508
- deliveredCanonicalEpisodeIds: [],
509
420
  deliveryMode: this.deliveryMode,
510
421
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
511
422
  };
@@ -520,7 +431,6 @@ export class PluginSession {
520
431
  retrievalFired: true,
521
432
  eligibleRefs: this.eligibleRefs,
522
433
  deliveredRefs: this.providedRefs,
523
- deliveredCanonicalEpisodeIds,
524
434
  deliveryMode: this.deliveryMode,
525
435
  deliveredContentHash: this.deliveredContentHash,
526
436
  ...(degradedReason !== undefined ? { degraded: degradedReason } : {}),
@@ -626,7 +536,8 @@ function renderKnowledgePacket(packet) {
626
536
  for (const excerpt of packet.excerpts)
627
537
  lines.push(`- ${excerpt.label}: ${excerpt.text}`);
628
538
  const capturedDate = packet.attribution.capturedAt.slice(0, 10);
629
- lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate}`);
539
+ lines.push(` source: ${packet.ref} · ${packet.attribution.origin} · captured ${capturedDate} · `
540
+ + `full episode: corpus_fetch ${packet.ref}`);
630
541
  return lines.join('\n');
631
542
  }
632
543
  function renderKnowledgePackets(packets) {
@@ -19,11 +19,6 @@ export interface CorpusRecordStep {
19
19
  */
20
20
  export interface CorpusRecord {
21
21
  ref: string;
22
- /**
23
- * Canonical EpisodeV1 episodeId, or the legacy trace sessionId used as its
24
- * read-compatible identity. Application metadata for deduplication only.
25
- */
26
- canonicalEpisodeId?: string;
27
22
  task: {
28
23
  summary: string;
29
24
  repositorySlug?: string;
@@ -2,7 +2,6 @@
2
2
  import { z } from 'zod';
3
3
  export declare const KnowledgeHitSchema: z.ZodObject<{
4
4
  ref: z.ZodString;
5
- canonicalEpisodeId: z.ZodOptional<z.ZodString>;
6
5
  kind: z.ZodEnum<{
7
6
  seed: "seed";
8
7
  trace: "trace";
@@ -23,7 +22,6 @@ export declare const KnowledgeHitSchema: z.ZodObject<{
23
22
  tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
23
  origin: z.ZodOptional<z.ZodString>;
25
24
  publishedAt: z.ZodOptional<z.ZodNumber>;
26
- recencyDomain: z.ZodOptional<z.ZodString>;
27
25
  retrievalVisible: z.ZodOptional<z.ZodBoolean>;
28
26
  }, z.core.$strict>;
29
27
  export type KnowledgeHit = z.infer<typeof KnowledgeHitSchema>;
@@ -3,7 +3,6 @@ import { z } from 'zod';
3
3
  import { TIER_ORDER } from './pickup-config.js';
4
4
  export const KnowledgeHitSchema = z.strictObject({
5
5
  ref: z.string().min(1),
6
- canonicalEpisodeId: z.string().min(1).optional(),
7
6
  kind: z.enum(['seed', 'trace', 'skill']),
8
7
  title: z.string().min(1).optional(),
9
8
  snippet: z.string().min(1).optional(),
@@ -15,15 +14,8 @@ export const KnowledgeHitSchema = z.strictObject({
15
14
  /** Trustworthy content-dedup identity: on-chain agentId when known, otherwise
16
15
  * the record ref. Never a manifest-supplied safeAddress. */
17
16
  origin: z.string().min(1).optional(),
18
- /** Adapter-native recency value — comparable only within `recencyDomain`. */
17
+ /** Unix-ms publish time — the selection policy's recency tiebreaker (rescope §3.3). */
19
18
  publishedAt: z.number().int().nonnegative().optional(),
20
- /**
21
- * Adapter-declared comparison domain for `publishedAt` (for example,
22
- * `unix-ms` or `block-number`). Recency is comparable only within one
23
- * explicit domain. When both hits omit this additive field, ranking keeps
24
- * the legacy raw-number behavior.
25
- */
26
- recencyDomain: z.string().min(1).optional(),
27
19
  /** Computed by the adapter from the wire hit's tags (issue #1824) — presence
28
20
  * of RETRIEVAL_VISIBLE_TAG. Absent = treat as not visible (fail-closed at
29
21
  * ranking). */
@@ -65,12 +65,11 @@ export interface KnowledgePacketBudget {
65
65
  maxChars?: number;
66
66
  }
67
67
  /**
68
- * Line-boundary-aware truncation ending with a neutral marker. The `ref`
69
- * parameter remains required for source compatibility with the public helper.
70
- * Returns an empty string when the budget cannot fit both meaningful source
71
- * text and the complete tail.
68
+ * Line-boundary-aware truncation ending with an explicit, pointer-bearing
69
+ * tail. Returns an empty string when the budget cannot fit both meaningful
70
+ * source text and the complete tail.
72
71
  */
73
- export declare function truncateLineBoundary(text: string, maxChars: number, _ref: string): string;
72
+ export declare function truncateLineBoundary(text: string, maxChars: number, ref: string): string;
74
73
  /**
75
74
  * Pure deterministic projection of a `CorpusRecord` into a `KnowledgePacket`
76
75
  * (rescope §3.2). Selects and truncates; never paraphrases. Enforces the
@@ -60,9 +60,6 @@ function stepDiff(step) {
60
60
  function stepNote(step) {
61
61
  return attrString(step.attributes, 'note') ?? attrString(step.attributes, 'turn.text');
62
62
  }
63
- function isAssistantTurn(step) {
64
- return attrString(step.attributes, 'role') === 'assistant';
65
- }
66
63
  const EXCERPT_LABELS = new Set([
67
64
  'failure', 'fix', 'command', 'diff', 'note',
68
65
  ]);
@@ -93,9 +90,7 @@ function seedStepExcerpt(step) {
93
90
  * output, the next command after it that is not itself a failure (the
94
91
  * correction), the last passing command overall (the final backstop), and a
95
92
  * diff step when present. Falls back to a single free-form note when
96
- * nothing command-shaped is found, preferring an assistant turn so a
97
- * conversational episode supplies the retained answer rather than merely
98
- * repeating the user's prompt. Selects only — never paraphrases.
93
+ * nothing command-shaped is found. Selects only — never paraphrases.
99
94
  */
100
95
  function selectExcerpts(steps) {
101
96
  const seedExcerpts = steps
@@ -140,23 +135,21 @@ function selectExcerpts(steps) {
140
135
  if (diffStep !== undefined)
141
136
  excerpts.push({ label: 'diff', text: stepDiff(diffStep) });
142
137
  if (excerpts.length === 0) {
143
- const noteStep = steps.find((step) => isAssistantTurn(step) && stepNote(step) !== undefined)
144
- ?? steps.find((step) => stepNote(step) !== undefined);
138
+ const noteStep = steps.find((step) => stepNote(step) !== undefined);
145
139
  if (noteStep !== undefined)
146
140
  excerpts.push({ label: 'note', text: stepNote(noteStep) });
147
141
  }
148
142
  return excerpts;
149
143
  }
150
144
  /**
151
- * Line-boundary-aware truncation ending with a neutral marker. The `ref`
152
- * parameter remains required for source compatibility with the public helper.
153
- * Returns an empty string when the budget cannot fit both meaningful source
154
- * text and the complete tail.
145
+ * Line-boundary-aware truncation ending with an explicit, pointer-bearing
146
+ * tail. Returns an empty string when the budget cannot fit both meaningful
147
+ * source text and the complete tail.
155
148
  */
156
- export function truncateLineBoundary(text, maxChars, _ref) {
149
+ export function truncateLineBoundary(text, maxChars, ref) {
157
150
  if (text.length <= maxChars)
158
151
  return text;
159
- const tail = '\n[truncated]';
152
+ const tail = `\n[truncated — full episode: corpus_fetch ${ref}]`;
160
153
  const budget = maxChars - tail.length;
161
154
  if (budget <= 0)
162
155
  return '';
@@ -6,7 +6,7 @@ import type { KnowledgeHit } from '../schemas/knowledge-hit.js';
6
6
  * search would return for it (kind/title/tier/payloadKind/publishedAt — the
7
7
  * fields not derivable from `CorpusRecord` alone).
8
8
  */
9
- export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt' | 'recencyDomain'>>;
9
+ export type InMemoryCorpusSeed = CorpusRecord & Pick<KnowledgeHit, 'kind'> & Partial<Pick<KnowledgeHit, 'title' | 'tier' | 'payloadKind' | 'publishedAt'>>;
10
10
  /** Map/array-backed, seedable — architecture spec §8. Seeds carry full
11
11
  * content (`CorpusRecord`) so `get()` can return it directly; `search()`
12
12
  * derives the lightweight `KnowledgeHit` view. */
@@ -17,7 +17,6 @@ export declare class InMemoryCorpusPort implements CorpusPort {
17
17
  ref: string;
18
18
  kind: "seed" | "trace" | "skill";
19
19
  tags: string[];
20
- canonicalEpisodeId?: string | undefined;
21
20
  title?: string | undefined;
22
21
  snippet?: string | undefined;
23
22
  score?: number | undefined;
@@ -25,7 +24,6 @@ export declare class InMemoryCorpusPort implements CorpusPort {
25
24
  payloadKind?: "unknown" | "skill" | undefined;
26
25
  origin?: string | undefined;
27
26
  publishedAt?: number | undefined;
28
- recencyDomain?: string | undefined;
29
27
  retrievalVisible?: boolean | undefined;
30
28
  }[]>>;
31
29
  get(ref: string): Promise<{
@@ -2,9 +2,6 @@ import { ok } from '../outcome.js';
2
2
  function toKnowledgeHit(seed) {
3
3
  return {
4
4
  ref: seed.ref,
5
- ...(seed.canonicalEpisodeId !== undefined
6
- ? { canonicalEpisodeId: seed.canonicalEpisodeId }
7
- : {}),
8
5
  kind: seed.kind,
9
6
  ...(seed.title !== undefined ? { title: seed.title } : {}),
10
7
  snippet: seed.task.summary,
@@ -13,7 +10,6 @@ function toKnowledgeHit(seed) {
13
10
  tags: seed.tags,
14
11
  origin: seed.origin,
15
12
  ...(seed.publishedAt !== undefined ? { publishedAt: seed.publishedAt } : {}),
16
- ...(seed.recencyDomain !== undefined ? { recencyDomain: seed.recencyDomain } : {}),
17
13
  retrievalVisible: seed.retrievalVisible,
18
14
  };
19
15
  }
@@ -52,7 +48,7 @@ export class InMemoryCorpusPort {
52
48
  if (!record)
53
49
  return ok(null);
54
50
  // Strip the KnowledgeHit-only fields — get() returns the CorpusRecord shape.
55
- const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, recencyDomain: _recencyDomain, ...corpusRecord } = record;
51
+ const { kind: _kind, title: _title, tier: _tier, payloadKind: _payloadKind, publishedAt: _publishedAt, ...corpusRecord } = record;
56
52
  return ok(corpusRecord);
57
53
  }
58
54
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinn-network/plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.0",
4
4
  "description": "Product core for the Jinn Plugin — ports, product schemas, and the createJinnPlugin factory. Stage 1 foundation package (contracts + in-memory kit only).",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinn-network/client",
3
- "version": "0.2.0-canary.0b79cd5d",
3
+ "version": "0.2.0-canary.31247316",
4
4
  "description": "Jinn protocol client — headless daemon for the restoration training loop",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@4.13.0",