@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.
- package/dist/build-info.json +3 -3
- package/dist/build-meta.json +1 -1
- package/dist/vendor/@jinn-network/core/package.json +2 -2
- package/dist/vendor/@jinn-network/plugin/dist/index.d.ts +1 -1
- package/dist/vendor/@jinn-network/plugin/dist/pickup.d.ts +3 -12
- package/dist/vendor/@jinn-network/plugin/dist/pickup.js +9 -45
- package/dist/vendor/@jinn-network/plugin/dist/plugin.d.ts +1 -5
- package/dist/vendor/@jinn-network/plugin/dist/plugin.js +32 -121
- package/dist/vendor/@jinn-network/plugin/dist/ports/corpus-port.d.ts +0 -5
- package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-hit.d.ts +0 -2
- package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-hit.js +1 -9
- package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-packet.d.ts +4 -5
- package/dist/vendor/@jinn-network/plugin/dist/schemas/knowledge-packet.js +7 -14
- package/dist/vendor/@jinn-network/plugin/dist/testing/in-memory-corpus.d.ts +1 -3
- package/dist/vendor/@jinn-network/plugin/dist/testing/in-memory-corpus.js +1 -5
- package/dist/vendor/@jinn-network/plugin/package.json +1 -1
- package/node_modules/@jinn-network/core/package.json +1 -1
- package/node_modules/@jinn-network/plugin/dist/index.d.ts +1 -1
- package/node_modules/@jinn-network/plugin/dist/pickup.d.ts +3 -12
- package/node_modules/@jinn-network/plugin/dist/pickup.js +9 -45
- package/node_modules/@jinn-network/plugin/dist/plugin.d.ts +1 -5
- package/node_modules/@jinn-network/plugin/dist/plugin.js +32 -121
- package/node_modules/@jinn-network/plugin/dist/ports/corpus-port.d.ts +0 -5
- package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-hit.d.ts +0 -2
- package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-hit.js +1 -9
- package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-packet.d.ts +4 -5
- package/node_modules/@jinn-network/plugin/dist/schemas/knowledge-packet.js +7 -14
- package/node_modules/@jinn-network/plugin/dist/testing/in-memory-corpus.d.ts +1 -3
- package/node_modules/@jinn-network/plugin/dist/testing/in-memory-corpus.js +1 -5
- package/node_modules/@jinn-network/plugin/package.json +1 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"clientGitSha": "
|
|
2
|
+
"clientGitSha": "312473165a5618f5f3e45a0b94532b63788916e1",
|
|
3
3
|
"codeDigest": "sha256:205201da01ad74e3474acce6b6c454c5e9aeab73ca0cba86124cd8547e057171",
|
|
4
|
-
"builtAt": "2026-07-
|
|
5
|
-
"implVersion": "0.2.0-canary.
|
|
4
|
+
"builtAt": "2026-07-24T09:04:59.282Z",
|
|
5
|
+
"implVersion": "0.2.0-canary.31247316"
|
|
6
6
|
}
|
package/dist/build-meta.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jinn-network/core",
|
|
3
|
-
"version": "0.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.
|
|
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,
|
|
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 →
|
|
85
|
-
*
|
|
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
|
|
296
|
+
function compareScoredKnowledgeHits(a, b) {
|
|
297
297
|
if (b.score !== a.score)
|
|
298
298
|
return b.score - a.score;
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 →
|
|
359
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
388
|
-
|
|
321
|
+
const result = nearMissResults[index];
|
|
322
|
+
prefetchedByRef.set(candidate.hit.ref, result);
|
|
323
|
+
if (result.status !== 'ok') {
|
|
389
324
|
if (degradedReason === undefined)
|
|
390
|
-
degradedReason =
|
|
325
|
+
degradedReason = result.reason;
|
|
391
326
|
continue;
|
|
392
327
|
}
|
|
393
|
-
|
|
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).
|
|
430
|
-
// disqualify a candidate without spending its slot, promoting
|
|
431
|
-
// next-ranked one: (1)
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
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
|
-
|
|
447
|
-
if (result
|
|
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
|
-
|
|
453
|
-
|
|
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 ${
|
|
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
|
-
/**
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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,
|
|
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
|
|
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) =>
|
|
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
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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,
|
|
149
|
+
export function truncateLineBoundary(text, maxChars, ref) {
|
|
157
150
|
if (text.length <= maxChars)
|
|
158
151
|
return text;
|
|
159
|
-
const tail =
|
|
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'
|
|
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,
|
|
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.
|
|
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.
|
|
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,
|
|
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 →
|
|
85
|
-
*
|
|
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
|
|
296
|
+
function compareScoredKnowledgeHits(a, b) {
|
|
297
297
|
if (b.score !== a.score)
|
|
298
298
|
return b.score - a.score;
|
|
299
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 →
|
|
359
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
388
|
-
|
|
321
|
+
const result = nearMissResults[index];
|
|
322
|
+
prefetchedByRef.set(candidate.hit.ref, result);
|
|
323
|
+
if (result.status !== 'ok') {
|
|
389
324
|
if (degradedReason === undefined)
|
|
390
|
-
degradedReason =
|
|
325
|
+
degradedReason = result.reason;
|
|
391
326
|
continue;
|
|
392
327
|
}
|
|
393
|
-
|
|
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).
|
|
430
|
-
// disqualify a candidate without spending its slot, promoting
|
|
431
|
-
// next-ranked one: (1)
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
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
|
-
|
|
447
|
-
if (result
|
|
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
|
-
|
|
453
|
-
|
|
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 ${
|
|
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
|
-
/**
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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,
|
|
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
|
|
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) =>
|
|
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
|
|
152
|
-
*
|
|
153
|
-
*
|
|
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,
|
|
149
|
+
export function truncateLineBoundary(text, maxChars, ref) {
|
|
157
150
|
if (text.length <= maxChars)
|
|
158
151
|
return text;
|
|
159
|
-
const tail =
|
|
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'
|
|
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,
|
|
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.
|
|
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