maka-agent 0.2.0-dev.41.20260918 → 0.2.0-dev.42.20260918
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/LICENSE +7 -4
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/client-plugin-bridge.js +19 -0
- package/node_modules/@maka/core/dist/events.js +58 -7
- package/node_modules/@maka/core/dist/interaction.js +38 -4
- package/node_modules/@maka/core/dist/llm-connections.js +4 -0
- package/node_modules/@maka/core/dist/model-metadata.js +89 -0
- package/node_modules/@maka/core/dist/recall.js +198 -15
- package/node_modules/@maka/core/dist/redaction.js +8 -0
- package/node_modules/@maka/core/dist/session-reference.js +149 -0
- package/node_modules/@maka/core/package.json +2 -0
- package/node_modules/@maka/runtime/dist/model-factory.js +5 -3
- package/node_modules/@maka/runtime/dist/model-history.js +13 -2
- package/node_modules/@maka/runtime/dist/plugin-client-bridge-service.js +308 -0
- package/node_modules/@maka/runtime/dist/plugin-composition-loader.js +4 -3
- package/node_modules/@maka/runtime/dist/plugin-data-services.js +3 -3
- package/node_modules/@maka/runtime/dist/recall-tools.js +70 -2
- package/node_modules/@maka/runtime/package.json +1 -0
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +7 -1
- package/node_modules/@maka/runtime-host/dist/protocol/operations.js +5 -0
- package/node_modules/@maka/runtime-host/dist/protocol/plugin-platform.js +269 -0
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +2 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +46 -6
- package/node_modules/@maka/runtime-host/dist/server/extension-package-manifest.js +19 -1
- package/node_modules/@maka/runtime-host/dist/server/plugin-package-loader.js +88 -14
- package/node_modules/@maka/runtime-host/dist/server/plugin-package-store.js +22 -5
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform-coordinator.js +122 -1
- package/node_modules/@maka/runtime-host/dist/server/plugin-platform.js +120 -1
- package/node_modules/@maka/runtime-host/dist/server/recall-material-fetch.js +92 -0
- package/node_modules/@maka/runtime-host/dist/server/root-turn-coordinator.js +14 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +17 -1
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-action-gate.js +67 -3
- package/node_modules/@maka/runtime-host/dist/server/workhub-coordination-coordinator.js +9 -3
- package/node_modules/@maka/runtime-host/dist/server/workhub-target-execution-authority.js +161 -0
- package/package.json +1 -1
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
* plus one weight that BM25 has no way to express: a tool result's term
|
|
53
53
|
* density reflects machine output rather than relevance.
|
|
54
54
|
*/
|
|
55
|
+
import { isCanonicalArtifactEntityId } from './artifacts.js';
|
|
56
|
+
import { formatAttachmentResourceRef, MAX_ATTACHMENT_COUNT } from './attachments.js';
|
|
57
|
+
import { isAttachmentRef } from './events.js';
|
|
55
58
|
import { validateWorkspacePrivacyContext } from './incognito.js';
|
|
56
59
|
import { redactSecrets } from './redaction.js';
|
|
57
60
|
import { SEARCH_QUERY_MAX_CHARS } from './search.js';
|
|
@@ -180,6 +183,76 @@ function stripSyntheticText(text, patterns) {
|
|
|
180
183
|
stripped = stripped.replace(pattern, '');
|
|
181
184
|
return stripped;
|
|
182
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* The files a message carried. A screenshot pasted under "have a look" leaves
|
|
188
|
+
* no trace in the text, so without this the message is unreachable by any
|
|
189
|
+
* term the user would think to search.
|
|
190
|
+
*/
|
|
191
|
+
export function recallMaterials(message) {
|
|
192
|
+
if (message.type !== 'user')
|
|
193
|
+
return [];
|
|
194
|
+
const attachments = message.attachments;
|
|
195
|
+
if (!Array.isArray(attachments) || attachments.length === 0)
|
|
196
|
+
return [];
|
|
197
|
+
// Shape first, then the cap: a record older or stranger than the current
|
|
198
|
+
// shape projects nothing, and a run of those ahead of a valid attachment
|
|
199
|
+
// must not spend the cap that valid one needed.
|
|
200
|
+
return attachments
|
|
201
|
+
.filter(isAttachmentRef)
|
|
202
|
+
.slice(0, MAX_ATTACHMENT_COUNT)
|
|
203
|
+
.map((attachment) => {
|
|
204
|
+
const resource = readableAttachmentResource(attachment);
|
|
205
|
+
const location = retrievableMaterialLocation(attachment);
|
|
206
|
+
return {
|
|
207
|
+
name: attachment.name,
|
|
208
|
+
kind: attachment.kind,
|
|
209
|
+
mimeType: attachment.mimeType,
|
|
210
|
+
bytes: attachment.bytes,
|
|
211
|
+
...(resource ? { resource } : {}),
|
|
212
|
+
...(location ?? {}),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The address `Read` accepts, present only for a material `Read` can actually
|
|
218
|
+
* return. `Read` answers an image with the image and anything else with the
|
|
219
|
+
* file's text, and refuses a PDF outright — so a PDF's address is one the
|
|
220
|
+
* caller can only fail on, which is the same reason a material in another
|
|
221
|
+
* Session carries no address.
|
|
222
|
+
*/
|
|
223
|
+
function readableAttachmentResource(attachment) {
|
|
224
|
+
if (attachment.kind === 'pdf')
|
|
225
|
+
return null;
|
|
226
|
+
return formatAttachmentResourceRef(attachment.ref);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The pair that names a material a retrieval tool can fetch: the Session that
|
|
230
|
+
* holds it and the artifact inside it. Only a durable Session artifact can be
|
|
231
|
+
* fetched, so a ref of any other kind names nothing.
|
|
232
|
+
*/
|
|
233
|
+
function retrievableMaterialLocation(attachment) {
|
|
234
|
+
if (attachment.ref.kind !== 'session_file')
|
|
235
|
+
return null;
|
|
236
|
+
if (!isCanonicalArtifactEntityId(attachment.ref.relativePath))
|
|
237
|
+
return null;
|
|
238
|
+
return {
|
|
239
|
+
sourceSessionId: attachment.ref.sessionId,
|
|
240
|
+
materialId: attachment.ref.relativePath,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What the predicate runs on: a message's prose plus the names of the files it
|
|
245
|
+
* carried, so a material is reachable by the name a person would remember.
|
|
246
|
+
* Names are matched but not returned as text — they come back as materials.
|
|
247
|
+
*/
|
|
248
|
+
function recallMatchableText(message) {
|
|
249
|
+
const prose = recallSearchableText(message);
|
|
250
|
+
const materials = recallMaterials(message);
|
|
251
|
+
if (materials.length === 0)
|
|
252
|
+
return prose;
|
|
253
|
+
const names = materials.map((material) => material.name).join('\n');
|
|
254
|
+
return prose === undefined || prose.length === 0 ? names : `${prose}\n${names}`;
|
|
255
|
+
}
|
|
183
256
|
/** Visits every string value in a JSON-like value, in document order, until the visitor declines. */
|
|
184
257
|
function collectStringLeaves(value, visit) {
|
|
185
258
|
if (typeof value === 'string')
|
|
@@ -261,7 +334,7 @@ export async function runRecall(request, deps, options = {}) {
|
|
|
261
334
|
collected.hits.sort((left, right) => right.score - left.score || compareHitOrder(left, right));
|
|
262
335
|
const sessionById = new Map(sessions.map((session) => [session.id, session]));
|
|
263
336
|
const anchors = applySessionQuota(collected.hits, limit);
|
|
264
|
-
const passages = assemblePassages(collected.transcripts, anchors, sessionById);
|
|
337
|
+
const passages = assemblePassages(collected.transcripts, anchors, sessionById, options.activeSessionId);
|
|
265
338
|
return {
|
|
266
339
|
ok: true,
|
|
267
340
|
facts,
|
|
@@ -334,7 +407,7 @@ export async function expandRecallPassage(request, deps, options = {}) {
|
|
|
334
407
|
if (!transcript) {
|
|
335
408
|
return { ok: false, reason: 'not_found', message: 'That Session was not found.' };
|
|
336
409
|
}
|
|
337
|
-
const message = transcript.find((candidate) => candidate.id === anchorMessageId &&
|
|
410
|
+
const message = transcript.find((candidate) => candidate.id === anchorMessageId && isPassageMessage(candidate));
|
|
338
411
|
if (!message) {
|
|
339
412
|
return { ok: false, reason: 'not_found', message: 'That passage anchor was not found.' };
|
|
340
413
|
}
|
|
@@ -355,12 +428,77 @@ export async function expandRecallPassage(request, deps, options = {}) {
|
|
|
355
428
|
tf: new Map(),
|
|
356
429
|
matchedTerms: [],
|
|
357
430
|
score: 0,
|
|
358
|
-
}, transcript, session, RECALL_PASSAGE_MAX_BYTES, { before, after });
|
|
431
|
+
}, transcript, session, RECALL_PASSAGE_MAX_BYTES, options.activeSessionId, { before, after });
|
|
359
432
|
if (!built) {
|
|
360
433
|
return { ok: false, reason: 'not_found', message: 'That passage could not be rebuilt.' };
|
|
361
434
|
}
|
|
362
435
|
return { ok: true, passage: built.passage };
|
|
363
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Brings one material named by a passage into the asking Session.
|
|
439
|
+
*
|
|
440
|
+
* The Session check is the point of this function, not a formality: without it
|
|
441
|
+
* the tool would be a way to read any artifact by id, including from the
|
|
442
|
+
* Sessions recall itself refuses to surface — incognito, retired simulator
|
|
443
|
+
* transcripts, whatever a future rule excludes. A material is retrievable
|
|
444
|
+
* exactly when recall could have shown you the Session it sits in.
|
|
445
|
+
*/
|
|
446
|
+
export async function fetchRecallMaterial(request, deps, options = {}) {
|
|
447
|
+
if (options.abortSignal?.aborted)
|
|
448
|
+
return aborted();
|
|
449
|
+
if (!deps.fetchMaterial) {
|
|
450
|
+
return { ok: false, reason: 'not_found', message: 'Materials are unavailable here.' };
|
|
451
|
+
}
|
|
452
|
+
if (typeof request !== 'object' || request === null || Array.isArray(request)) {
|
|
453
|
+
return { ok: false, reason: 'invalid_query', message: 'Material request must be an object.' };
|
|
454
|
+
}
|
|
455
|
+
const record = request;
|
|
456
|
+
const sessionId = optionalString(record.sessionId);
|
|
457
|
+
const materialId = optionalString(record.materialId);
|
|
458
|
+
if (!sessionId || !materialId) {
|
|
459
|
+
return {
|
|
460
|
+
ok: false,
|
|
461
|
+
reason: 'invalid_query',
|
|
462
|
+
message: 'A material is named by its Session and its material id.',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (!options.activeSessionId) {
|
|
466
|
+
return { ok: false, reason: 'not_found', message: 'Materials need an active Session.' };
|
|
467
|
+
}
|
|
468
|
+
const privacyPayload = await deps.getPrivacyContext();
|
|
469
|
+
if (options.abortSignal?.aborted)
|
|
470
|
+
return aborted();
|
|
471
|
+
const privacy = validateWorkspacePrivacyContext(privacyPayload);
|
|
472
|
+
if (!privacy.ok || privacy.value.incognitoActive) {
|
|
473
|
+
return {
|
|
474
|
+
ok: false,
|
|
475
|
+
reason: 'incognito_active',
|
|
476
|
+
message: 'Recall is unavailable while incognito is active.',
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const sessions = eligibleSessions(collapseSessionRevisions(await deps.listSessions(), options.activeSessionId), { sessionId, includeArchived: options.includeArchived === true });
|
|
480
|
+
if (options.abortSignal?.aborted)
|
|
481
|
+
return aborted();
|
|
482
|
+
if (!sessions.some((candidate) => candidate.id === sessionId)) {
|
|
483
|
+
return { ok: false, reason: 'not_found', message: 'That material was not found.' };
|
|
484
|
+
}
|
|
485
|
+
const fetched = await deps.fetchMaterial({
|
|
486
|
+
sourceSessionId: sessionId,
|
|
487
|
+
materialId,
|
|
488
|
+
targetSessionId: options.activeSessionId,
|
|
489
|
+
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
|
|
490
|
+
});
|
|
491
|
+
if (options.abortSignal?.aborted)
|
|
492
|
+
return aborted();
|
|
493
|
+
if (!fetched.ok) {
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
reason: fetched.reason === 'unsupported' ? 'invalid_query' : 'not_found',
|
|
497
|
+
message: fetched.message,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return { ok: true, content: fetched.content };
|
|
501
|
+
}
|
|
364
502
|
function normalizeSpan(value) {
|
|
365
503
|
if (value === undefined)
|
|
366
504
|
return RECALL_EXPAND_MAX_NEIGHBOURS;
|
|
@@ -602,7 +740,7 @@ function verify(sessionId, message, input) {
|
|
|
602
740
|
if (sessionId === input.activeSessionId && turnId && input.excludeTurnIds?.has(turnId)) {
|
|
603
741
|
return undefined;
|
|
604
742
|
}
|
|
605
|
-
const raw =
|
|
743
|
+
const raw = recallMatchableText(message);
|
|
606
744
|
if (raw === undefined)
|
|
607
745
|
return undefined;
|
|
608
746
|
// A hit is a term that occurs in the text as it was stored *and* still
|
|
@@ -738,7 +876,7 @@ function applySessionQuota(hits, limit) {
|
|
|
738
876
|
// first when the total cap binds.
|
|
739
877
|
return selected.sort((left, right) => right.score - left.score);
|
|
740
878
|
}
|
|
741
|
-
function assemblePassages(transcripts, anchors, sessionById) {
|
|
879
|
+
function assemblePassages(transcripts, anchors, sessionById, activeSessionId) {
|
|
742
880
|
if (anchors.length === 0)
|
|
743
881
|
return [];
|
|
744
882
|
const passages = [];
|
|
@@ -749,7 +887,7 @@ function assemblePassages(transcripts, anchors, sessionById) {
|
|
|
749
887
|
const transcript = transcripts.get(anchor.sessionId);
|
|
750
888
|
if (!transcript)
|
|
751
889
|
continue;
|
|
752
|
-
const built = buildPassage(anchor, transcript, sessionById.get(anchor.sessionId), remaining);
|
|
890
|
+
const built = buildPassage(anchor, transcript, sessionById.get(anchor.sessionId), remaining, activeSessionId);
|
|
753
891
|
if (!built)
|
|
754
892
|
continue;
|
|
755
893
|
remaining -= built.bytes;
|
|
@@ -757,7 +895,7 @@ function assemblePassages(transcripts, anchors, sessionById) {
|
|
|
757
895
|
}
|
|
758
896
|
return passages;
|
|
759
897
|
}
|
|
760
|
-
function buildPassage(anchor, transcript, session, budget, span = {
|
|
898
|
+
function buildPassage(anchor, transcript, session, budget, activeSessionId, span = {
|
|
761
899
|
before: RECALL_PASSAGE_NEIGHBOURS,
|
|
762
900
|
after: RECALL_PASSAGE_NEIGHBOURS,
|
|
763
901
|
}) {
|
|
@@ -779,7 +917,7 @@ function buildPassage(anchor, transcript, session, budget, span = {
|
|
|
779
917
|
const render = (message, isAnchor) => {
|
|
780
918
|
if (rendered.has(message.id))
|
|
781
919
|
return;
|
|
782
|
-
const projected = projectPassageMessage(message, isAnchor);
|
|
920
|
+
const projected = projectPassageMessage(message, isAnchor, activeSessionId);
|
|
783
921
|
if (!projected)
|
|
784
922
|
return;
|
|
785
923
|
const overhead = Buffer.byteLength(JSON.stringify({ ...projected, text: '' }), 'utf8');
|
|
@@ -816,8 +954,7 @@ function buildPassage(anchor, transcript, session, budget, span = {
|
|
|
816
954
|
// the caller can still reach, so it counts toward the continuation flags the
|
|
817
955
|
// same way one beyond the span does. Reporting otherwise would tell a caller
|
|
818
956
|
// there is nothing more in a direction recall just cut short.
|
|
819
|
-
const omitted = (entries) => entries.some((entry) => !rendered.has(entry.message.id) &&
|
|
820
|
-
projectPassageMessage(entry.message, false) !== undefined);
|
|
957
|
+
const omitted = (entries) => entries.some((entry) => !rendered.has(entry.message.id) && isPassageMessage(entry.message));
|
|
821
958
|
const passage = {
|
|
822
959
|
sessionId: anchor.sessionId,
|
|
823
960
|
sessionTitle: redactSecrets(session?.name ?? ''),
|
|
@@ -877,13 +1014,36 @@ function collectNeighbours(transcript, anchorIndex, turnId, span) {
|
|
|
877
1014
|
function isPassageNeighbour(message) {
|
|
878
1015
|
return message.type === 'user' || message.type === 'assistant' || message.type === 'tool_call';
|
|
879
1016
|
}
|
|
880
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* Whether a message can appear in a passage at all. Redaction rewrites text
|
|
1019
|
+
* but never empties it, so this decides the same set `projectPassageMessage`
|
|
1020
|
+
* does without paying for redaction on every message a lookup walks past.
|
|
1021
|
+
*/
|
|
1022
|
+
function isPassageMessage(message) {
|
|
881
1023
|
const raw = recallSearchableText(message);
|
|
882
|
-
if (raw
|
|
883
|
-
return
|
|
884
|
-
|
|
885
|
-
|
|
1024
|
+
if (raw !== undefined && raw.trim().length > 0)
|
|
1025
|
+
return true;
|
|
1026
|
+
return recallMaterials(message).length > 0;
|
|
1027
|
+
}
|
|
1028
|
+
function projectPassageMessage(message, isAnchor, activeSessionId) {
|
|
1029
|
+
const raw = recallSearchableText(message);
|
|
1030
|
+
const text = raw === undefined ? '' : redactSecrets(raw).trim();
|
|
1031
|
+
// A message whose whole content was a pasted file has no text of its own.
|
|
1032
|
+
// Dropping it would make the file unreachable in exactly the case this
|
|
1033
|
+
// layer exists for.
|
|
1034
|
+
// A file name is user-authored text like any other, so it leaves through the
|
|
1035
|
+
// same redaction the passage body does; the address beside it is a runtime
|
|
1036
|
+
// identifier and carries nothing to redact.
|
|
1037
|
+
const materials = recallMaterials(message).map((material) => ({
|
|
1038
|
+
...material,
|
|
1039
|
+
name: redactSecrets(material.name),
|
|
1040
|
+
}));
|
|
1041
|
+
if (text.length === 0 && materials.length === 0)
|
|
886
1042
|
return undefined;
|
|
1043
|
+
// A material carries an address or a location, never both: the address says
|
|
1044
|
+
// "read this now", the location says "ask for it and it will be brought
|
|
1045
|
+
// here". Offering an address `Read` would refuse is the mistake this split
|
|
1046
|
+
// exists to prevent.
|
|
887
1047
|
return {
|
|
888
1048
|
messageId: message.id,
|
|
889
1049
|
role: passageRole(message),
|
|
@@ -891,6 +1051,29 @@ function projectPassageMessage(message, isAnchor) {
|
|
|
891
1051
|
text,
|
|
892
1052
|
timestamp: message.ts,
|
|
893
1053
|
isAnchor,
|
|
1054
|
+
...(materials.length > 0
|
|
1055
|
+
? { materials: materials.map((material) => addressOrLocation(material, activeSessionId)) }
|
|
1056
|
+
: {}),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Reachability is a fact about where the material itself is stored, not about
|
|
1061
|
+
* the passage that mentioned it. Keying it on the passage would offer an
|
|
1062
|
+
* address for a ref pointing into another Session — the doomed address this
|
|
1063
|
+
* split exists to prevent, with the polarity inverted.
|
|
1064
|
+
*/
|
|
1065
|
+
function addressOrLocation(material, activeSessionId) {
|
|
1066
|
+
const { resource, sourceSessionId, materialId, ...named } = material;
|
|
1067
|
+
if (resource && sourceSessionId !== undefined && sourceSessionId === activeSessionId) {
|
|
1068
|
+
return { ...named, resource };
|
|
1069
|
+
}
|
|
1070
|
+
// A location is a thing to ask for, so it is only worth carrying when
|
|
1071
|
+
// asking could succeed. A PDF is refused wherever it is stored.
|
|
1072
|
+
return {
|
|
1073
|
+
...named,
|
|
1074
|
+
...(sourceSessionId && materialId && named.kind !== 'pdf'
|
|
1075
|
+
? { sourceSessionId, materialId }
|
|
1076
|
+
: {}),
|
|
894
1077
|
};
|
|
895
1078
|
}
|
|
896
1079
|
function passageRole(message) {
|
|
@@ -197,6 +197,14 @@ export function classifyGeneralizedError(error) {
|
|
|
197
197
|
return 'timeout';
|
|
198
198
|
if (lower.includes('429') || lower.includes('rate'))
|
|
199
199
|
return 'rate_limited';
|
|
200
|
+
// builder-util-runtime appends generic authentication-token advice to HTTP
|
|
201
|
+
// 404 errors. electron-updater has already classified this particular case
|
|
202
|
+
// as a missing channel artifact, so it is not evidence of bad credentials.
|
|
203
|
+
if (typeof error === 'object' &&
|
|
204
|
+
error !== null &&
|
|
205
|
+
'code' in error &&
|
|
206
|
+
error.code === 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND')
|
|
207
|
+
return undefined;
|
|
200
208
|
if (lower.includes('401') || lower.includes('403') || isAuthenticationErrorText(lower))
|
|
201
209
|
return 'auth_failed';
|
|
202
210
|
if (/\b5\d\d\b/.test(lower))
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
3
|
+
* or more contributor license agreements. See the NOTICE file
|
|
4
|
+
* distributed with this work for additional information
|
|
5
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
6
|
+
* to you under the Apache License, Version 2.0 (the
|
|
7
|
+
* "License"); you may not use this file except in compliance
|
|
8
|
+
* with the License. You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing,
|
|
13
|
+
* software distributed under the License is distributed on an
|
|
14
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
15
|
+
* KIND, either express or implied. See the License for the
|
|
16
|
+
* specific language governing permissions and limitations
|
|
17
|
+
* under the License.
|
|
18
|
+
*/
|
|
19
|
+
import { redactSecrets } from './redaction.js';
|
|
20
|
+
import { userFacingText } from './session.js';
|
|
21
|
+
const utf8Encoder = new TextEncoder();
|
|
22
|
+
/** Default per-reference budget. It is deliberately small enough to leave room for the active task. */
|
|
23
|
+
export const SESSION_SNAPSHOT_DEFAULT_MAX_CHARS = 12_000;
|
|
24
|
+
export const SESSION_SNAPSHOT_MAX_CHARS = 32_000;
|
|
25
|
+
export const SESSION_SNAPSHOT_MAX_BYTES = 32_000;
|
|
26
|
+
export const SESSION_SNAPSHOT_MAX_ITEMS = 24;
|
|
27
|
+
/**
|
|
28
|
+
* Build the model-safe portion of a Session transcript.
|
|
29
|
+
*
|
|
30
|
+
* Only user and assistant text is shareable. Tool calls/results, permission
|
|
31
|
+
* events, system notes, and other Runtime records remain outside this
|
|
32
|
+
* projection. Items are selected from the tail, then restored to transcript
|
|
33
|
+
* order so a large history cannot crowd the current request unexpectedly.
|
|
34
|
+
*/
|
|
35
|
+
export function createSessionSnapshot(messages, options) {
|
|
36
|
+
const maxChars = clampPositiveInteger(options.maxChars ?? SESSION_SNAPSHOT_DEFAULT_MAX_CHARS, 1, SESSION_SNAPSHOT_MAX_CHARS);
|
|
37
|
+
const maxItems = clampPositiveInteger(options.maxItems ?? SESSION_SNAPSHOT_MAX_ITEMS, 1, SESSION_SNAPSHOT_MAX_ITEMS);
|
|
38
|
+
const candidates = messages.flatMap((message) => {
|
|
39
|
+
if (message.type !== 'user' && message.type !== 'assistant')
|
|
40
|
+
return [];
|
|
41
|
+
const text = message.type === 'user' ? userFacingText(message) : message.text;
|
|
42
|
+
const normalized = redactSecrets(text).trim();
|
|
43
|
+
if (!normalized)
|
|
44
|
+
return [];
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
role: message.type,
|
|
48
|
+
text: normalized,
|
|
49
|
+
turnId: message.turnId,
|
|
50
|
+
ts: message.ts,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
});
|
|
54
|
+
const selected = [];
|
|
55
|
+
let usedChars = 0;
|
|
56
|
+
let usedBytes = 0;
|
|
57
|
+
let truncated = false;
|
|
58
|
+
for (let index = candidates.length - 1; index >= 0 && selected.length < maxItems; index -= 1) {
|
|
59
|
+
const candidate = candidates[index];
|
|
60
|
+
const line = formatSnapshotItem(candidate);
|
|
61
|
+
const separator = selected.length > 0 ? 2 : 0;
|
|
62
|
+
const available = maxChars - usedChars - separator;
|
|
63
|
+
const availableBytes = SESSION_SNAPSHOT_MAX_BYTES - usedBytes - separator;
|
|
64
|
+
if (available <= 0 || availableBytes <= 0) {
|
|
65
|
+
truncated = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const lineBytes = utf8Encoder.encode(line).byteLength;
|
|
69
|
+
if (line.length <= available && lineBytes <= availableBytes) {
|
|
70
|
+
selected.push(candidate);
|
|
71
|
+
usedChars += separator + line.length;
|
|
72
|
+
usedBytes += separator + lineBytes;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (selected.length === 0) {
|
|
76
|
+
const prefix = formatSnapshotItem({ ...candidate, text: '' });
|
|
77
|
+
const contentBudget = Math.max(0, available - prefix.length);
|
|
78
|
+
const byteBudget = Math.max(0, availableBytes - utf8Encoder.encode(prefix).byteLength);
|
|
79
|
+
if (contentBudget > 0 && byteBudget > 0) {
|
|
80
|
+
const text = sliceAtUtf8Boundary(sliceAtCodePointBoundary(candidate.text, contentBudget), byteBudget).trimEnd();
|
|
81
|
+
if (!text) {
|
|
82
|
+
truncated = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
selected.push({
|
|
86
|
+
...candidate,
|
|
87
|
+
text,
|
|
88
|
+
});
|
|
89
|
+
usedChars = maxChars;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
truncated = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (selected.length < candidates.length)
|
|
96
|
+
truncated = true;
|
|
97
|
+
selected.reverse();
|
|
98
|
+
const text = sliceAtCodePointBoundary(selected.map(formatSnapshotItem).join('\n\n'), maxChars);
|
|
99
|
+
return {
|
|
100
|
+
reference: {
|
|
101
|
+
sessionId: options.sessionId,
|
|
102
|
+
sessionName: redactSecrets(options.sessionName),
|
|
103
|
+
capturedAt: options.capturedAt ?? Date.now(),
|
|
104
|
+
},
|
|
105
|
+
items: selected,
|
|
106
|
+
text,
|
|
107
|
+
estimatedTokens: Math.ceil(text.length / 4),
|
|
108
|
+
maxChars,
|
|
109
|
+
truncated,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Convert a snapshot into the existing inline quote transport. */
|
|
113
|
+
export function sessionSnapshotToQuote(snapshot) {
|
|
114
|
+
const sessionName = redactSecrets(snapshot.reference.sessionName);
|
|
115
|
+
return {
|
|
116
|
+
text: snapshot.text,
|
|
117
|
+
label: sliceAtCodePointBoundary(`Session: ${sessionName}`, 200),
|
|
118
|
+
sourceSessionId: snapshot.reference.sessionId,
|
|
119
|
+
sourceSessionName: sliceAtCodePointBoundary(sessionName, 200),
|
|
120
|
+
sourceCapturedAt: snapshot.reference.capturedAt,
|
|
121
|
+
sourceTruncated: snapshot.truncated,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function formatSnapshotItem(item) {
|
|
125
|
+
return `${item.role === 'user' ? 'User' : 'Assistant'}: ${item.text}`;
|
|
126
|
+
}
|
|
127
|
+
/** Keep a UTF-16 slice from ending between the halves of a surrogate pair. */
|
|
128
|
+
function sliceAtCodePointBoundary(value, maxCodeUnits) {
|
|
129
|
+
const sliced = value.slice(0, maxCodeUnits);
|
|
130
|
+
const last = sliced.charCodeAt(sliced.length - 1);
|
|
131
|
+
return last >= 0xd800 && last <= 0xdbff ? sliced.slice(0, -1) : sliced;
|
|
132
|
+
}
|
|
133
|
+
function sliceAtUtf8Boundary(value, maxBytes) {
|
|
134
|
+
let bytes = 0;
|
|
135
|
+
let end = 0;
|
|
136
|
+
for (const codePoint of value) {
|
|
137
|
+
const nextBytes = utf8Encoder.encode(codePoint).byteLength;
|
|
138
|
+
if (bytes + nextBytes > maxBytes)
|
|
139
|
+
break;
|
|
140
|
+
bytes += nextBytes;
|
|
141
|
+
end += codePoint.length;
|
|
142
|
+
}
|
|
143
|
+
return value.slice(0, end);
|
|
144
|
+
}
|
|
145
|
+
function clampPositiveInteger(value, min, max) {
|
|
146
|
+
if (!Number.isSafeInteger(value))
|
|
147
|
+
return min;
|
|
148
|
+
return Math.max(min, Math.min(max, value));
|
|
149
|
+
}
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"./provider-retry-countdown": "./dist/provider-retry-countdown.js",
|
|
30
30
|
"./interaction": "./dist/interaction.js",
|
|
31
31
|
"./session": "./dist/session.js",
|
|
32
|
+
"./session-reference": "./dist/session-reference.js",
|
|
32
33
|
"./session-revisions": "./dist/session-revisions.js",
|
|
33
34
|
"./collaboration": "./dist/collaboration.js",
|
|
34
35
|
"./orchestration": "./dist/orchestration.js",
|
|
@@ -66,6 +67,7 @@
|
|
|
66
67
|
"./capabilities": "./dist/capabilities.js",
|
|
67
68
|
"./capability-audit": "./dist/capability-audit.js",
|
|
68
69
|
"./client-capability-grant": "./dist/client-capability-grant.js",
|
|
70
|
+
"./client-plugin-bridge": "./dist/client-plugin-bridge.js",
|
|
69
71
|
"./health": "./dist/health.js",
|
|
70
72
|
"./search": "./dist/search.js",
|
|
71
73
|
"./bot-events": "./dist/bot-events.js",
|
|
@@ -654,11 +654,13 @@ function buildFamilyWire(connection, modelId, level, thinkingOptions, requestedL
|
|
|
654
654
|
: {},
|
|
655
655
|
};
|
|
656
656
|
case 'commandcode-cli':
|
|
657
|
-
// The CLI wire takes `reasoning_effort` as the CLI's own effort words
|
|
658
|
-
// `max`
|
|
657
|
+
// The CLI wire takes `reasoning_effort` as the CLI's own effort words.
|
|
658
|
+
// `max` is one of them (claude-fable-5-1, moonshotai/Kimi-K3, …), so the
|
|
659
|
+
// chosen level is forwarded verbatim — rounding it down would silently
|
|
660
|
+
// send a weaker request than the user asked for.
|
|
659
661
|
return level === undefined || level === 'off'
|
|
660
662
|
? {}
|
|
661
|
-
: { 'commandcode-cli': { reasoningEffort: level
|
|
663
|
+
: { 'commandcode-cli': { reasoningEffort: level } };
|
|
662
664
|
default:
|
|
663
665
|
return {};
|
|
664
666
|
}
|
|
@@ -799,8 +799,19 @@ function formatAttachmentRefs(attachments) {
|
|
|
799
799
|
function formatQuoteRefs(quotes) {
|
|
800
800
|
return quotes
|
|
801
801
|
.map((q) => {
|
|
802
|
-
const
|
|
803
|
-
|
|
802
|
+
const attributes = [
|
|
803
|
+
q.label === undefined ? undefined : `label="${quoteAttribute(q.label)}"`,
|
|
804
|
+
q.sourceSessionId === undefined
|
|
805
|
+
? undefined
|
|
806
|
+
: `source_session="${quoteAttribute(q.sourceSessionId)}"`,
|
|
807
|
+
q.sourceCapturedAt === undefined ? undefined : `captured_at="${q.sourceCapturedAt}"`,
|
|
808
|
+
q.sourceTruncated === undefined ? undefined : `truncated="${q.sourceTruncated}"`,
|
|
809
|
+
].filter((attribute) => attribute !== undefined);
|
|
810
|
+
const opening = attributes.length > 0 ? `<quoted_excerpt ${attributes.join(' ')}>` : '<quoted_excerpt>';
|
|
811
|
+
return `${opening}\n${q.text}\n</quoted_excerpt>`;
|
|
804
812
|
})
|
|
805
813
|
.join('\n');
|
|
806
814
|
}
|
|
815
|
+
function quoteAttribute(value) {
|
|
816
|
+
return value.replace(/["<&>]/g, (character) => character === '"' ? "'" : `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
817
|
+
}
|