@yeaft/webchat-agent 1.0.513 → 1.0.515

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.513",
3
+ "version": "1.0.515",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -827,6 +827,20 @@ class SegmentStore {
827
827
  : out.sort(compareMessagesBySeq);
828
828
  }
829
829
 
830
+ /**
831
+ * Read physical rows without applying reflection tombstones. Session cloning
832
+ * needs the complete durable transcript, including rows hidden by folding.
833
+ */
834
+ readAllRaw({ includeCold = false } = {}) {
835
+ if (!this.hasData()) return [];
836
+ const idx = this.loadIndex();
837
+ return (idx.segments || [])
838
+ .slice()
839
+ .sort((a, b) => (a.firstSeq || 0) - (b.firstSeq || 0))
840
+ .flatMap(segment => this.#readSegment(segment.file, { includeCold }))
841
+ .sort(compareMessagesBySeq);
842
+ }
843
+
830
844
  *scan({ beforeSeq = Infinity, afterSeq = -Infinity, desc = false, includeCold = false, scanStats = null } = {}) {
831
845
  if (!this.hasData()) return;
832
846
  const idx = this.loadIndex();
@@ -1615,6 +1629,81 @@ export class ConversationStore {
1615
1629
  return this.loadRecentBySession(sessionId, Infinity);
1616
1630
  }
1617
1631
 
1632
+ /**
1633
+ * Copy the complete durable transcript to a new Session identity.
1634
+ *
1635
+ * Unlike visible history readers, this includes cold rows, internal rows,
1636
+ * reflections, and the rows hidden by reflection tombstones. New persisted
1637
+ * message ids are allocated in original order. References that point to a
1638
+ * copied persisted message are remapped; external/client/tool identities are
1639
+ * intentionally preserved.
1640
+ *
1641
+ * @returns {{ copiedCount: number, idMap: Map<string, string> }}
1642
+ */
1643
+ copySession(sourceSessionId, targetSessionId) {
1644
+ if (!sourceSessionId || !targetSessionId || sourceSessionId === targetSessionId) {
1645
+ return { copiedCount: 0, idMap: new Map() };
1646
+ }
1647
+ const primary = this.#segmentStoreForConversationDir(this.#sessionConversationDir(sourceSessionId));
1648
+ const segmentedRows = primary.readAllRaw({ includeCold: true });
1649
+ const legacyRows = this.#sessionFileEntries('all', sourceSessionId)
1650
+ .map(entry => {
1651
+ try { return this.readMessageFile(entry.path); } catch (err) {
1652
+ if (isPermissionError(err)) return null;
1653
+ throw err;
1654
+ }
1655
+ })
1656
+ .filter(Boolean);
1657
+ // Migration can temporarily leave the same durable row in both the legacy
1658
+ // markdown layout and the segment store. Preserve legacy-only rows, but let
1659
+ // the canonical segment copy win when both contain the same persisted id.
1660
+ const rowsById = new Map();
1661
+ for (const row of [...legacyRows, ...segmentedRows]) {
1662
+ if (row?.sessionId !== sourceSessionId || typeof row.id !== 'string' || !row.id) continue;
1663
+ rowsById.set(row.id, row);
1664
+ }
1665
+ const rows = [...rowsById.values()].sort(compareMessagesBySeq);
1666
+ if (rows.length === 0) return { copiedCount: 0, idMap: new Map() };
1667
+
1668
+ const firstSeq = this.#getNextSeq();
1669
+ const idMap = new Map(rows.map((row, index) => [
1670
+ row.id,
1671
+ `m${String(firstSeq + index).padStart(4, '0')}`,
1672
+ ]));
1673
+ const remapId = id => idMap.get(id) || id;
1674
+ const copies = rows.map(row => {
1675
+ const copy = { ...row, sessionId: targetSessionId };
1676
+ delete copy.id;
1677
+ if (idMap.has(row.causalRootId)) copy.causalRootId = remapId(row.causalRootId);
1678
+ if (Array.isArray(row.foldedMessageIds)) {
1679
+ copy.foldedMessageIds = row.foldedMessageIds.map(remapId);
1680
+ }
1681
+ if (Array.isArray(row.sourceMessageIds)) {
1682
+ copy.sourceMessageIds = row.sourceMessageIds.map(remapId);
1683
+ }
1684
+ if (row.cold === true) delete copy.cold;
1685
+ return copy;
1686
+ });
1687
+ const written = this.appendBatch(copies);
1688
+ for (let index = 0; index < written.length; index += 1) {
1689
+ if (rows[index].cold === true) this.moveToCold(written[index].id);
1690
+ }
1691
+
1692
+ // append() intentionally treats permission failures as best-effort for live
1693
+ // chat. A Session copy cannot: reporting success with a partial transcript
1694
+ // would make the new Session irrecoverably incomplete. Verify the target's
1695
+ // physical rows before the higher-level CRUD operation commits the clone.
1696
+ const target = this.#segmentStoreForConversationDir(this.#sessionConversationDir(targetSessionId));
1697
+ const persistedIds = new Set(target.readAllRaw({ includeCold: true }).map(row => row.id));
1698
+ const expectedIds = [...idMap.values()];
1699
+ if (written.length !== rows.length
1700
+ || persistedIds.size !== expectedIds.length
1701
+ || expectedIds.some(id => !persistedIds.has(id))) {
1702
+ throw new Error(`Session transcript copy incomplete: expected ${rows.length}, persisted ${persistedIds.size}`);
1703
+ }
1704
+ return { copiedCount: written.length, idMap };
1705
+ }
1706
+
1618
1707
  /**
1619
1708
  * VP-scoped view of Session history, used to build a pair-safe provider
1620
1709
  * snapshot for one VP. It operates on what the VP can actually see, not
package/yeaft/engine.js CHANGED
@@ -1250,8 +1250,8 @@ export class Engine {
1250
1250
  }
1251
1251
 
1252
1252
  /**
1253
- * Build the system prompt with the AMS-rendered Memory block, the
1254
- * Active Scope block, and skill content. The legacy multi-path
1253
+ * Build the system prompt with the AMS-rendered Memory block, routing
1254
+ * metadata, and skill content. The legacy multi-path
1255
1255
  * Memory injection (FTS-formatted + AMS snapshot + Layer-A summaries +
1256
1256
  * userProfile + coreMemory) was retired in DESIGN-PROMPT v1; callers
1257
1257
  * now thread a single `memoryInjection` string composed upstream from
@@ -1260,8 +1260,7 @@ export class Engine {
1260
1260
  * Routes through `buildWorkerPrompt`, which:
1261
1261
  * - Lays in the persona-as-identity block (or Yeaft identity fallback)
1262
1262
  * - Adds the Memory section (passed in as `memoryInjection`)
1263
- * - Adds the structured Active Scope block (`activeScope`)
1264
- * - Forwards optional `taskCtx` for the legacy task-context sub-block
1263
+ * - Adds multi-VP routing metadata from `activeScope` when applicable
1265
1264
  *
1266
1265
  * @param {object} args
1267
1266
  * @param {string} args.prompt — user prompt (for skill relevance matching)
@@ -1273,11 +1272,10 @@ export class Engine {
1273
1272
  * @param {string} [args.projectLabel] — current Project name and id for prompt attribution
1274
1273
  * @param {string} [args.workCenterInstructions] — frozen Agent-level Work Center policy
1275
1274
  * @param {string} [args.projectDoc] — resolved CLAUDE.md / AGENTS.md text (already truncated)
1276
- * @param {object} [args.taskCtx] — legacy task-context sub-block (optional)
1277
1275
  * @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
1278
1276
  * @returns {string}
1279
1277
  */
1280
- #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, taskCtx, activeTasks, collabToolPolicy = null, activeToolNames = null, promptNotices = [], explicitSkillName, resolvedSkillContent = null } = {}) {
1278
+ #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, promptNotices = [], explicitSkillName, resolvedSkillContent = null } = {}) {
1281
1279
  // #runQuery resolves Skill selection at each provider-request boundary so
1282
1280
  // a live Plugin policy change cannot leave stale content in the next prompt.
1283
1281
  // Keep the local fallback for internal callers that do not need selection
@@ -1292,21 +1290,8 @@ export class Engine {
1292
1290
  }
1293
1291
  }
1294
1292
 
1295
- // Prompt guidance must describe the same canonical capability
1296
- // intersection that reaches provider schemas and execution.
1297
- const registeredToolNames = this.#toolRegistry
1298
- ? this.#toolRegistry.getToolNames({
1299
- plugins: this.#config?.plugins,
1300
- collabToolPolicy,
1301
- })
1302
- : Array.from(this.#tools.keys());
1303
- const toolNames = activeToolNames instanceof Set
1304
- ? registeredToolNames.filter(name => activeToolNames.has(name))
1305
- : registeredToolNames;
1306
-
1307
1293
  return buildWorkerPrompt({
1308
1294
  language: this.#config.language || 'en',
1309
- toolNames,
1310
1295
  memoryInjection,
1311
1296
  skillContent,
1312
1297
  vpPersona,
@@ -1317,8 +1302,6 @@ export class Engine {
1317
1302
  workCenterInstructions,
1318
1303
  projectDoc,
1319
1304
  runtimePlatform: getRuntimePlatformInfo(),
1320
- taskCtx,
1321
- activeTasks,
1322
1305
  promptNotices,
1323
1306
  // Worker-shape harness is descriptive metadata for human inspection;
1324
1307
  // production prompts skip it to save tokens. Re-enable via env when
@@ -2306,18 +2289,13 @@ export class Engine {
2306
2289
  }))
2307
2290
  : [];
2308
2291
 
2309
- // ─── Active Scope (DESIGN-PROMPT §3 ④) ──────────────────────
2310
- // Structured per-turn scope summary: session + vp + members + envelope routing
2311
- // info. Long-form scope content lives in AMS — this block carries
2312
- // only IDs + tiny labels. (Feature scope retired 2026-05-13.)
2313
- const activeSessionTopics = topicScopesForResident
2314
- .map(scope => scope.replace(/^sessions\/[^/]+\/topic\//, ''));
2292
+ // ─── Multi-VP routing scope ──────────────────────────────────
2293
+ // Only identity and peer membership affect the routing contract rendered in
2294
+ // the system prompt. Session bookkeeping and inferred topics remain in their
2295
+ // owning runtime/memory paths instead of being mirrored into prompt metadata.
2315
2296
  const activeScope = {
2316
- sessionId: sessionId || '',
2317
- sessionMember: ownVpIdForAms || '',
2297
+ vpId: ownVpIdForAms || '',
2318
2298
  sessionMembers: Array.isArray(sessionMembers) ? sessionMembers : [],
2319
- sessionTopics: activeSessionTopics,
2320
- envelope: inboundEnvelope || null,
2321
2299
  };
2322
2300
 
2323
2301
  let projectDocSource = this.#getProjectDocBlock(workDir);
@@ -2332,11 +2310,6 @@ export class Engine {
2332
2310
  && typeof this.#taskManager.listActiveTasks === 'function'
2333
2311
  ? this.#taskManager.listActiveTasks(runtimeSessionId)
2334
2312
  : [];
2335
- let activeTasks = this.#taskManager
2336
- ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
2337
- language: this.#config.language || 'en',
2338
- })
2339
- : '';
2340
2313
  const registeredToolNames = this.#toolRegistry
2341
2314
  ? this.#toolRegistry.getToolNames({
2342
2315
  collabToolPolicy: effectiveCollabToolPolicy,
@@ -2407,9 +2380,6 @@ export class Engine {
2407
2380
  projectLabel,
2408
2381
  workCenterInstructions,
2409
2382
  projectDoc: projectDocContext.text,
2410
- activeTasks,
2411
- collabToolPolicy: effectiveCollabToolPolicy,
2412
- activeToolNames,
2413
2383
  promptNotices,
2414
2384
  explicitSkillName,
2415
2385
  resolvedSkillContent,
@@ -2825,17 +2795,18 @@ export class Engine {
2825
2795
  && typeof this.#taskManager.listActiveTasks === 'function'
2826
2796
  ? this.#taskManager.listActiveTasks(runtimeSessionId)
2827
2797
  : [];
2828
- activeTasks = this.#taskManager
2829
- ? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId, {
2830
- language: this.#config.language || 'en',
2831
- })
2832
- : '';
2833
2798
  activeToolNames = resolveCurrentActiveToolNames();
2834
2799
  for (const name of [...discoveredToolNames]) {
2835
2800
  if (this.#toolRegistry?.has(name)) activeToolNames?.add(name);
2836
2801
  else discoveredToolNames.delete(name);
2837
2802
  }
2838
2803
  toolDefs = this.#getToolDefs(effectiveCollabToolPolicy, activeToolNames);
2804
+ // Only a child registry supplies this policy. Budget exhaustion closes
2805
+ // investigation, not the evidence-bearing conversation: reserve one
2806
+ // tool-free response for a useful handoff, under the existing signal.
2807
+ const executionPolicy = isSubAgent
2808
+ ? this.#toolRegistry?.prepareProviderRequest?.() : null;
2809
+ if (executionPolicy?.finalize) toolDefs = [];
2839
2810
  ({ resolvedSkillContent, resolvedSkills, skillResolutionError } = resolveSkillPromptState({
2840
2811
  skillManager: this.#skillManager,
2841
2812
  prompt,
@@ -2853,6 +2824,7 @@ export class Engine {
2853
2824
  reportedSkillNames = currentSkillNames;
2854
2825
  reportedSkillError = skillResolutionError;
2855
2826
  systemPrompt = buildCurrentSystemPrompt();
2827
+ if (executionPolicy?.prompt) systemPrompt += `\n\n${executionPolicy.prompt}`;
2856
2828
 
2857
2829
  try {
2858
2830
  // Resolve effort per provider request so a saved Session effort takes
@@ -2966,6 +2938,7 @@ export class Engine {
2966
2938
  const requestMaxOutputTokens = Math.max(1, Math.min(
2967
2939
  requestConfig.maxOutputTokens || resolveMaxOutputTokens(currentModel, requestConfig),
2968
2940
  resolveMaxOutputTokens(currentModel, requestConfig),
2941
+ executionPolicy?.maxOutputTokens || Infinity,
2969
2942
  ));
2970
2943
  const toolSchemaTokens = toolDefs.length > 0
2971
2944
  ? approxTokens(JSON.stringify(toolDefs)) : 0;
@@ -3059,7 +3032,12 @@ export class Engine {
3059
3032
  retryLifecycle.pendingContinuation = null;
3060
3033
  continuationCommitted = true;
3061
3034
  };
3035
+ let childDispatchReserved = false;
3062
3036
  const commitDispatch = () => {
3037
+ if (isSubAgent && !childDispatchReserved) {
3038
+ this.#toolRegistry?.reserveProviderRequest?.({ reporting: !!executionPolicy?.finalize });
3039
+ childDispatchReserved = true;
3040
+ }
3063
3041
  if (!activeProviderRequest && typeof prepareProviderRequest === 'function') {
3064
3042
  activeProviderRequest = prepareProviderRequest({
3065
3043
  turnNumber,
@@ -3166,6 +3144,9 @@ export class Engine {
3166
3144
  }
3167
3145
  break;
3168
3146
  case 'tool_call':
3147
+ // No dispatch or orphan protocol rows during the reserved report,
3148
+ // even if an adapter ignores the absence of tool definitions.
3149
+ if (executionPolicy?.finalize) break;
3169
3150
  if (toolCalls.length === 0) {
3170
3151
  traceRequest('llm.first_tool_call', {
3171
3152
  durationMs: perfNowMs() - requestPerfStart,
@@ -3409,7 +3390,7 @@ export class Engine {
3409
3390
  // the caller. Replaying that request would publish a duplicate call and
3410
3391
  // leave ambiguous execution ownership, so only pre-tool failures are
3411
3392
  // eligible for transparent retry or model fallback.
3412
- const canReplayProviderRequest = toolCalls.length === 0;
3393
+ const canReplayProviderRequest = toolCalls.length === 0 && !executionPolicy?.finalize;
3413
3394
  if (earlyIsContextOverflow && canReplayProviderRequest
3414
3395
  && contextOverflowRecoveryAttempts < 3) {
3415
3396
  contextOverflowRecoveryAttempts += 1;
@@ -3808,6 +3789,14 @@ export class Engine {
3808
3789
  conversationMessages.push(assistantMsg);
3809
3790
  fullResponseText += responseText;
3810
3791
 
3792
+ // The reporting allowance is exactly one provider response, not another
3793
+ // investigation loop. Ignore unsolicited calls even from a noncompliant
3794
+ // adapter, and never auto-continue a truncated reporting response.
3795
+ if (executionPolicy?.finalize) {
3796
+ yield { type: 'turn_end', turnNumber, stopReason: 'budget_report', threadId, terminal: true };
3797
+ break;
3798
+ }
3799
+
3811
3800
  // ─── Handle max_tokens → auto-continue ────────────
3812
3801
  // A suppressed call leaves a synthetic reminder as the latest user
3813
3802
  // message. Some models answer it with an empty end_turn. Continue exactly