@genesislcap/ai-assistant 15.86.1 → 15.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/ai-assistant.api.json +5026 -645
  2. package/dist/ai-assistant.d.ts +802 -0
  3. package/dist/chat-driver.cjs +1487 -132
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +1480 -132
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +2779 -657
  8. package/dist/dts/autopilot/autopilot-session.d.ts +25 -0
  9. package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -1
  10. package/dist/dts/chat-driver-node.d.ts +2 -0
  11. package/dist/dts/chat-driver-node.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/ai-driver.d.ts +15 -0
  13. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  14. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +6 -0
  15. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +7 -0
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/interaction-controller.d.ts +12 -0
  19. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  20. package/dist/dts/headless/describe-interaction.d.ts +17 -0
  21. package/dist/dts/headless/describe-interaction.d.ts.map +1 -0
  22. package/dist/dts/headless/headless-debug.d.ts +28 -0
  23. package/dist/dts/headless/headless-debug.d.ts.map +1 -0
  24. package/dist/dts/headless/headless-persistence.d.ts +35 -0
  25. package/dist/dts/headless/headless-persistence.d.ts.map +1 -0
  26. package/dist/dts/headless/headless-session.d.ts +180 -0
  27. package/dist/dts/headless/headless-session.d.ts.map +1 -0
  28. package/dist/dts/headless/headless.types.d.ts +435 -0
  29. package/dist/dts/headless/headless.types.d.ts.map +1 -0
  30. package/dist/dts/headless/index.d.ts +5 -0
  31. package/dist/dts/headless/index.d.ts.map +1 -0
  32. package/dist/dts/headless-server/compact.d.ts +52 -0
  33. package/dist/dts/headless-server/compact.d.ts.map +1 -0
  34. package/dist/dts/headless-server/index.d.ts +23 -0
  35. package/dist/dts/headless-server/index.d.ts.map +1 -0
  36. package/dist/dts/headless-server/server.d.ts +13 -0
  37. package/dist/dts/headless-server/server.d.ts.map +1 -0
  38. package/dist/dts/headless-server/types.d.ts +210 -0
  39. package/dist/dts/headless-server/types.d.ts.map +1 -0
  40. package/dist/dts/index.d.ts +2 -0
  41. package/dist/dts/index.d.ts.map +1 -1
  42. package/dist/dts/main/main.d.ts.map +1 -1
  43. package/dist/dts/state/debug-event-log.d.ts +1 -1
  44. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  45. package/dist/dts/state/decision-surface.d.ts +5 -4
  46. package/dist/dts/state/decision-surface.d.ts.map +1 -1
  47. package/dist/dts/state/persistence/debug-log-assembly.d.ts +49 -0
  48. package/dist/dts/state/persistence/debug-log-assembly.d.ts.map +1 -0
  49. package/dist/dts/state/persistence/index.d.ts +1 -0
  50. package/dist/dts/state/persistence/index.d.ts.map +1 -1
  51. package/dist/dts/state/persistence/session-persister.d.ts +7 -0
  52. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  53. package/dist/dts/types/interaction-surface.d.ts +30 -0
  54. package/dist/dts/types/interaction-surface.d.ts.map +1 -1
  55. package/dist/esm/autopilot/autopilot-session.js +31 -16
  56. package/dist/esm/chat-driver-node.js +8 -0
  57. package/dist/esm/components/ai-driver/base-driver-decorator.js +12 -0
  58. package/dist/esm/components/chat-driver/chat-driver.js +8 -0
  59. package/dist/esm/components/chat-driver/interaction-controller.js +66 -0
  60. package/dist/esm/headless/describe-interaction.js +55 -0
  61. package/dist/esm/headless/headless-debug.js +62 -0
  62. package/dist/esm/headless/headless-persistence.js +192 -0
  63. package/dist/esm/headless/headless-session.js +808 -0
  64. package/dist/esm/headless/headless.types.js +1 -0
  65. package/dist/esm/headless/index.js +3 -0
  66. package/dist/esm/headless-server/compact.js +160 -0
  67. package/dist/esm/headless-server/index.js +21 -0
  68. package/dist/esm/headless-server/server.js +746 -0
  69. package/dist/esm/headless-server/types.js +3 -0
  70. package/dist/esm/index.js +3 -0
  71. package/dist/esm/main/main.js +18 -37
  72. package/dist/esm/state/debug-event-log.js +6 -1
  73. package/dist/esm/state/decision-surface.js +75 -48
  74. package/dist/esm/state/persistence/debug-log-assembly.js +69 -0
  75. package/dist/esm/state/persistence/index.js +1 -0
  76. package/dist/esm/state/persistence/session-persister.js +16 -0
  77. package/dist/headless-server.cjs +853 -0
  78. package/dist/headless-server.cjs.map +7 -0
  79. package/dist/headless-server.mjs +819 -0
  80. package/dist/headless-server.mjs.map +7 -0
  81. package/package.json +28 -18
  82. package/scripts/build-headless-server-node.mjs +42 -0
  83. package/scripts/smoke-chat-driver-node.mjs +3 -0
  84. package/scripts/smoke-headless-server-node.mjs +112 -0
@@ -0,0 +1,3 @@
1
+ export const DECISION_TEXTS = ['full', 'lean'];
2
+ /** What the server uses when neither it nor the command says. Its readers are models that can `read` a view. */
3
+ export const DEFAULT_DECISION_TEXT = 'lean';
package/dist/esm/index.js CHANGED
@@ -4,6 +4,9 @@ export * from './main/main.template';
4
4
  export * from './components/agent-picker';
5
5
  export * from './components/agent-session';
6
6
  export * from './autopilot';
7
+ export * from './headless';
8
+ // Persistence for a headless session reaches the session store, so it is here and not in the Node entry.
9
+ export { headlessPersistence, } from './headless/headless-persistence';
7
10
  export * from './components/ai-driver';
8
11
  export * from './components/chat-driver';
9
12
  export * from './components/orchestrating-driver';
@@ -49,11 +49,11 @@ import { agentNotificationPermission, asAgentNotificationLevel, belongsToSession
49
49
  import { AgentNotifier } from '../notifications/agent-notifier';
50
50
  import { addNotificationSettingsHost, announceNotificationSettings, } from '../notifications/notification-settings';
51
51
  import { AssistantAppSettingsProvider, } from '../provider/assistant-app-settings';
52
- import { recordMetaEvent, getMetaEvents, clearSession as clearMetaSession, DEBUG_LOG_README, } from '../state/debug-event-log';
52
+ import { recordMetaEvent, clearSession as clearMetaSession, DEBUG_LOG_README, } from '../state/debug-event-log';
53
53
  import { getOrCreateDriver, getDriver, getDriverAgentsKey, deleteDriver, } from '../state/driver-registry';
54
54
  import { getInteractionRegistration } from '../state/interaction-registry';
55
- import { buildTimelineEntries } from '../state/persistence/build-timeline-entries';
56
- import { assembleDebugLog, withFreshMetaSnapshot } from '../state/persistence/diagnostics';
55
+ import { buildDiagnosticEntries, lifetimeDebugLog } from '../state/persistence/debug-log-assembly';
56
+ import { assembleDebugLog } from '../state/persistence/diagnostics';
57
57
  import { deleteDiagnosticsCursorsFor, resetDiagnosticsCursorsFor, } from '../state/persistence/diagnostics-cursors';
58
58
  import { getOrCreatePersister, getPersister, deletePersister, } from '../state/persistence/persister-registry';
59
59
  import { resumeVerdict } from '../state/persistence/resume-verdict';
@@ -4059,25 +4059,12 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
4059
4059
  * been appended to the persisted stream.
4060
4060
  */
4061
4061
  buildDiagnosticEntries() {
4062
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
4063
- const stateKey = this.getStateKey();
4064
- // The message/turn/event timeline entries — built by the shared, pure `buildTimelineEntries`
4065
- // (the same helper a headless consumer uses to harvest its own log), from the driver's pull
4066
- // surfaces. Prefer the driver's raw history (carries sub-agent traces) over the redux projection.
4067
- const timelineEntries = buildTimelineEntries({
4068
- turnSnapshots: (_c = (_b = (_a = this.driver) === null || _a === void 0 ? void 0 : _a.getTurnSnapshots) === null || _b === void 0 ? void 0 : _b.call(_a)) !== null && _c !== void 0 ? _c : [],
4069
- messages: (_f = (_e = (_d = this.driver) === null || _d === void 0 ? void 0 : _d.getRawHistory) === null || _e === void 0 ? void 0 : _e.call(_d)) !== null && _f !== void 0 ? _f : this.messages,
4070
- metaEvents: stateKey ? getMetaEvents(stateKey) : [],
4062
+ return buildDiagnosticEntries({
4063
+ driver: this.driver,
4064
+ stateKey: this.getStateKey(),
4065
+ metaSnapshot: () => this.buildMetaSnapshot(),
4066
+ messages: this.messages,
4071
4067
  });
4072
- // Fold in any external diagnostics harvested from an out-of-band driver (e.g. a server-side
4073
- // ChatDriver whose collated log an interaction widget returned on its result). They ride the
4074
- // same download + persisted-diagnostics path; `assembleDebugLog` sorts the whole timeline by
4075
- // timestamp so they interleave chronologically. (GENC-1461 unified diagnostics.)
4076
- return [
4077
- ...timelineEntries,
4078
- ...((_j = (_h = (_g = this.driver) === null || _g === void 0 ? void 0 : _g.getExternalDiagnostics) === null || _h === void 0 ? void 0 : _h.call(_g)) !== null && _j !== void 0 ? _j : []),
4079
- this.buildMetaSnapshot(),
4080
- ];
4081
4068
  }
4082
4069
  /**
4083
4070
  * The `meta-snapshot` entry for right now. The assembly is a pure transformation
@@ -4383,23 +4370,17 @@ let FoundationAiAssistant = FoundationAiAssistant_1 = class FoundationAiAssistan
4383
4370
  */
4384
4371
  getLifetimeDebugLog() {
4385
4372
  return __awaiter(this, void 0, void 0, function* () {
4386
- var _a;
4387
- const provider = this.persistence.provider;
4388
- const key = this.getStateKey();
4389
- if ((provider === null || provider === void 0 ? void 0 : provider.loadDiagnostics) && key) {
4390
- try {
4391
- // Land the current page's unflushed delta first so the download includes it.
4373
+ return lifetimeDebugLog({
4374
+ provider: this.persistence.provider,
4375
+ stateKey: this.getStateKey(),
4376
+ // Land the current page's unflushed delta first so the download includes it.
4377
+ flush: () => __awaiter(this, void 0, void 0, function* () {
4378
+ var _a;
4392
4379
  yield ((_a = this.persister()) === null || _a === void 0 ? void 0 : _a.flushDiagnostics());
4393
- const stored = yield provider.loadDiagnostics(key);
4394
- if (stored.length) {
4395
- return assembleDebugLog(withFreshMetaSnapshot(stored, this.buildMetaSnapshot()), DEBUG_LOG_README);
4396
- }
4397
- }
4398
- catch (e) {
4399
- logger.error('Diagnostics load failed — using current-page log:', e);
4400
- }
4401
- }
4402
- return this.getDebugLog();
4380
+ }),
4381
+ metaSnapshot: () => this.buildMetaSnapshot(),
4382
+ fallback: () => this.getDebugLog(),
4383
+ });
4403
4384
  });
4404
4385
  }
4405
4386
  /**
@@ -108,6 +108,11 @@ export const META_EVENT_IMPORTANCE = {
108
108
  'autopilot.catch-up': 'normal',
109
109
  // Autopilot spend that reached no message, so it is missing from the session total.
110
110
  'autopilot.cost-dropped': 'high',
111
+ // A headless session attached to the driver: from here a host with no screen is the person.
112
+ 'headless.attached': 'normal',
113
+ // The host settled an interaction raw, without the widget's own actions: its result went to the
114
+ // tool as given, unchecked by anything the widget would have enforced.
115
+ 'headless.raw-resolved': 'normal',
111
116
  'external-diagnostics.folded': 'normal',
112
117
  'session.cleared': 'normal',
113
118
  'session.switched': 'low',
@@ -264,7 +269,7 @@ export const DEBUG_LOG_README = [
264
269
  "kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
265
270
  "kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
266
271
  "Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, subagent.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions, sub-agent start/complete), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated, context.condensed). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
267
- 'Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted/ai-service-error, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached — the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, for ai-service-error the code (one of AI_SERVICE_ERROR_CODES, e.g. NO_API_KEY) and the status the AI service answered with — its own text goes to the browser console only, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted/ai_service_error; budget_exhausted and provider_refused are terminal for the PARENT turn too, and so is ai_service_error when the refusal is about the service rather than the request and came from the vendor the parent calls — the parent stops rather than calling the model again into the same wall; after context_exhausted the parent goes on, but its own next call is likely to stop on the same guard), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), routing.decided (ONE PER ROUTING DECISION — see its own paragraph below), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver; detail.ownerPath names the agents that actually asked, outermost first, the last being the one whose tool requested it, and detail.heldBy says who held it when it opened: user, or the id of the interceptor that claimed it, such as autopilot; on interaction.resolved, detail.answeredBy is who answered — the claimant id when it settled through its own hold, user for any other answer (a click counts as the person even on a widget a claimant held) — absent when it closed unanswered (a timeout or a stop), and absent too when it settled by itself other than through the hold of a claimant — a widget registered resolvesItself (one that does its work and settles on its own, asking the person nothing, such as a generation run), or one outcome its controller said settled itself (ctx.resolve with resolvedItself, such as a generation phase that failed outright before its review gate ever asked anything) — which then carries detail.resolvedItself: true instead, as the interaction message does — nobody answered it, and the platform cannot tell it finishing from the person interrupting it (a Skip), so only the result of the widget itself can say which; the same value is stamped on the interaction message as answeredBy and on the result the asking tool handler receives (absent there in the same cases), and when it is not user the tool result that interaction returned carries a decidedOnUsersBehalf record, so the model was told the choice was not made by the person; detail.edits, present only when non-zero, counts the editing actions a claimant ran on the widget before it was settled, whoever settled it, and those edits are stamped on the interaction message as edits, each with by, action, choice and args when it had some, in the order they ran, while a decidedOnUsersBehalf record carries their choice texts as edits, and when the person settled it after such edits the tool result carries an editedOnUsersBehalf record instead — the framing and those choice texts — so the model knows the widget was changed on the user behalf before the user answered, and that the user then confirmed it by finishing it), interaction.held (a claimant moved a pending interaction to the person or back to itself — detail.heldBy is the new holder, detail.claimant the interceptor; detail.late: true is an interceptor that declined it when it opened claiming it afterwards, such as the autopilot resumed while it was open; a widget held by anything but user was not answered by the person), interaction.claim-refused (a claim on a userOnly interaction, which stays with the person — detail.claimant + reason), autopilot.* (the autopilot, a decorator that answers interactions on behalf of the user — autopilot.claimed / autopilot.declined say whether it took each interaction and, for a decline, detail.reason: paused, other-flow, no-autopilot, unavailable, userOnly, policy or no-controller; one declined as paused that is still open when the person resumes the autopilot is looked at again, and the autopilot.claimed or autopilot.declined recorded for it then carries detail.onResume: true (a decline then can also be claimed-elsewhere, another interceptor having claimed it first); a restored mode looks at nothing; autopilot.decision-sent is what it was sent (detail.chars; detail.nothingEnabled: true when every action on the widget was unavailable — a gate whose controller said it was asking, such as a failed build the autopilot may not cancel — so it could only hand it back, which it may then do even after the person asked it to decide it itself; a widget whose controller says it is working (ctx.setWorking) is never sent a decision until it says it is asking, even when an action is enabled or the person resumes the autopilot); autopilot.acted is what it did (detail.action, detail.note, detail.finished); autopilot.handed-back is a decision it gave to the person (detail.reason and detail.trigger: model, no-decision or error); autopilot.paused / autopilot.resumed are mode changes (detail.scope, detail.trigger, detail.decideYourself when the person resumed during a hand-back; detail.trigger is restored when the mode came back with the session after a reload, applied once its flow re-activated, which starts nothing by itself); autopilot.carried is the mode, running or paused, going with the task to the agent that took it over, rather than ending with the flow as on any other change of flow owner: to the target of a hand-off, or back to its caller on the hand-back (detail.from, detail.to, detail.mode); a pause the person chose goes with it, and Stop and Clear still end it; a mode restored for the target of a hand-off applies to its caller when the reload hands back; autopilot.error is the wrapper itself failing; autopilot.refinement counts a refining action against its step — a written answer (reply) or a widget action declared refines, such as a regenerate with instructions, counted together (detail.action, detail.stepKey, detail.count, detail.limitReached, after which every refining action is withdrawn for that step: not listed, and refused; the decision and every relisting of its actions say how many are left at the step, as a line Refinements left at this step: N, wherever the widget has a refining action); autopilot.handback-presented is the turn-level hand-back, shown when a working turn ended needing the person with no widget to hand over (detail.why: max-iterations, context-exhausted, failure or question; detail.options; detail.choice once answered); autopilot.catch-up is the autopilot being given the whole visible transcript at once (detail.why: first, for its first decision in a flow or after a reload; size, when its own history passed its budget and was rebuilt short — detail.ownChars; or compacted, when a compaction removed the last message it had seen, so it reads the summary and everything since, short; in each, what it had sent on the user behalf is included and labelled as its own, since its own history does not hold it); autopilot.cost-dropped is autopilot spend that no message carries, so it is NOT in the session total (detail.costUsd and detail.why: handed-back, aborted or stale for a written question it answered that no reply was sent for — stale when a pause, a resume, a new turn or the end of its flow came before the answer landed, which then went nowhere — or no-recorder when the driver had no way to book a decision to its interaction); a hand-back trigger can also be budget (the decision passed its spend cap), watchdog (its work went quiet: no ready surface and no heartbeat, or its own decision stalled with no model call landing, which stops that decision) or detached (the autopilot was removed while it held the interaction, which went to the person with no reason shown). To debug one autopilot decision, follow its interactionId: interaction.requested (heldBy), autopilot.claimed, autopilot.decision-sent, autopilot.acted or autopilot.handed-back, interaction.invoked, interaction.resolved (answeredBy), then the next turn of the working agent), interaction.invoked (a claimant ran one of the actions the widget itself exposes, through its hold, the same code the button runs — detail.claimant, detail.action (an option id, select or reply) and detail.finished, whether that settled the interaction, and when it did not, the run is recorded as an edit; the choice and narration it recorded reach the result of the tool that asked as decidedOnUsersBehalf), interaction.step (a long action run as named steps through runSteps, which survive a reload — detail.step and detail.outcome: started, done, skipped when the step was already done before a reload, or resumed when it started before a reload and runs again with what it saved part-way, such as a job id it re-attaches to), interaction.checkpoint (the checkpoints a conversation keeps for its long actions: the one in flight, and each one a reload cut, held apart until a request of its own widget type takes it over, so a resumed flow that opens other long widgets first, which save their own, loses nothing — detail.action: resumed when a reload cut the work and a request of the same widget type that the registration sameWork agrees is the same work took it over, refused when sameWork said a new request is different work (detail.saved is the identity it kept; detail.kept: true, since it stays for a later request of that type, and is offered to every one until one agrees) or when runSteps found the handed-over checkpoint malformed and started fresh (detail.reason, detail.saved), or cleared when its flow ended, once per checkpoint, with detail.cut: true for one a reload cut that nothing took over), context.updated/threshold-crossed (token + cost), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.',
272
+ "Event types: assistant.connected/disconnected (mount + placement + whether the session was created or restored), assistant.popout/popin (window placement), driver.created/wired/unwired (which driver is live and why it stops/starts responding across a popout), state.changed (idle↔loading), turn.start/turn.end (turn boundary; turn.end carries durationMs), turn.retry (a recoverable in-turn retry — detail.reason plus attempt/maxAttempts; for malformed calls also finishMessage; for empty responses also the provider finishReason + thoughtsTokens + parts breakdown), turn.error (a turn failed or hit a guardrail — detail.reason is one of exception/malformed-function-call/empty-response/unknown-tool-limit/max-iterations/response-truncated/refusal/budget-exhausted/provider-refused/context-exhausted/ai-service-error, plus reason-specific diagnostics: attempts (for empty-response also finishReason + thoughtsTokens + a parts breakdown, distinguishing a thinking-only STOP from a truly empty turn), finishMessage, for response-truncated the model + maxTokens + outputTokens + tools, unknownTools (split into staleTools — real earlier this activation but retired by the current state or hidden behind an open exclusive fold — and hallucinatedTools — never advertised) + availableTools, iterations + limit, for budget-exhausted the budgetUsd + spentUsd figures reported by the proxy plus the resolved vendor, for provider-refused the kind (spend|auth) plus the upstream status and error type — the vendor refusing the ACCOUNT rather than us refusing to spend, so the remedy is a top-up or a key rotation rather than a raised cap, and the kind is the only place that distinction survives because the user-facing copy is deliberately cause-free, for context-exhausted the estimated contextTokens against the guard that stopped the turn plus the iterations reached — the driver ending a runaway tool loop at the edge of the context window rather than issuing a request the provider would reject, so the transcript survives intact and can still be compacted, for ai-service-error the code (one of AI_SERVICE_ERROR_CODES, e.g. NO_API_KEY) and the status the AI service answered with — its own text goes to the browser console only, or name + message for exceptions), tool.failed (a tool threw), tool.unresolved (the model called a tool that could not be dispatched — detail.kind is folded/fold-hidden/stale/unknown, plus tool + agent and, for the counted kinds, the consecutive streak; the recurring lead-up to an unknown-tool-limit turn.error), subagent.started/completed/failed (the lifecycle of a `requestSubAgent` delegation — detail.agent names the sub-agent; these bracket the sub-agent turns that appear as kind:turn entries with an N-M `turnIndex`; subagent.failed also carries detail.reason, one of max_iterations/malformed_tool_call/empty_response/unknown_tool_limit/timeout/response_truncated/refusal/budget_exhausted/provider_refused/context_exhausted/ai_service_error; budget_exhausted and provider_refused are terminal for the PARENT turn too, and so is ai_service_error when the refusal is about the service rather than the request and came from the vendor the parent calls — the parent stops rather than calling the model again into the same wall; after context_exhausted the parent goes on, but its own next call is likely to stop on the same guard), agent.handoff (routing; from=null is the initial activation), agent.pinned/unpinned (forced routing), routing.decided (ONE PER ROUTING DECISION — see its own paragraph below), provider.selected (the resolved provider for the upcoming turns — detail.provider is the registry SLOT/tier name, detail.model the concrete model behind it and detail.vendor its vendor; emitted only when the slot CHANGES, so read the per-turn `model` for the model of any given call rather than assuming the nearest event still applies), interaction.requested/resolved (blocking user widgets — explain quiet gaps; note that when a sub-agent opens a widget, detail.agent — and the agentName on the interaction message — is the HOST agent that owns the widget, NOT the sub-agent that asked, because widgets render and resolve on the host driver; detail.ownerPath names the agents that actually asked, outermost first, the last being the one whose tool requested it, and detail.heldBy says who held it when it opened: user, or the id of the interceptor that claimed it, such as autopilot; on interaction.resolved, detail.answeredBy is who answered — the claimant id when it settled through its own hold, user for any other answer (a click counts as the person even on a widget a claimant held) — absent when it closed unanswered (a timeout or a stop), and absent too when it settled by itself other than through the hold of a claimant — a widget registered resolvesItself (one that does its work and settles on its own, asking the person nothing, such as a generation run), or one outcome its controller said settled itself (ctx.resolve with resolvedItself, such as a generation phase that failed outright before its review gate ever asked anything) — which then carries detail.resolvedItself: true instead, as the interaction message does — nobody answered it, and the platform cannot tell it finishing from the person interrupting it (a Skip), so only the result of the widget itself can say which; the same value is stamped on the interaction message as answeredBy and on the result the asking tool handler receives (absent there in the same cases), and when it is not user the tool result that interaction returned carries a decidedOnUsersBehalf record, so the model was told the choice was not made by the person; detail.edits, present only when non-zero, counts the editing actions a claimant ran on the widget before it was settled, whoever settled it, and those edits are stamped on the interaction message as edits, each with by, action, choice and args when it had some, in the order they ran, while a decidedOnUsersBehalf record carries their choice texts as edits, and when the person settled it after such edits the tool result carries an editedOnUsersBehalf record instead — the framing and those choice texts — so the model knows the widget was changed on the user behalf before the user answered, and that the user then confirmed it by finishing it), interaction.held (a claimant moved a pending interaction to the person or back to itself — detail.heldBy is the new holder, detail.claimant the interceptor; detail.late: true is an interceptor that declined it when it opened claiming it afterwards, such as the autopilot resumed while it was open; a widget held by anything but user was not answered by the person), interaction.claim-refused (a claim on a userOnly interaction, which stays with the person — detail.claimant + reason), autopilot.* (the autopilot, a decorator that answers interactions on behalf of the user — autopilot.claimed / autopilot.declined say whether it took each interaction and, for a decline, detail.reason: paused, other-flow, no-autopilot, unavailable, userOnly, policy or no-controller; one declined as paused that is still open when the person resumes the autopilot is looked at again, and the autopilot.claimed or autopilot.declined recorded for it then carries detail.onResume: true (a decline then can also be claimed-elsewhere, another interceptor having claimed it first); a restored mode looks at nothing; autopilot.decision-sent is what it was sent (detail.chars; detail.nothingEnabled: true when every action on the widget was unavailable — a gate whose controller said it was asking, such as a failed build the autopilot may not cancel — so it could only hand it back, which it may then do even after the person asked it to decide it itself; a widget whose controller says it is working (ctx.setWorking) is never sent a decision until it says it is asking, even when an action is enabled or the person resumes the autopilot); autopilot.acted is what it did (detail.action, detail.note, detail.finished); autopilot.handed-back is a decision it gave to the person (detail.reason and detail.trigger: model, no-decision or error); autopilot.paused / autopilot.resumed are mode changes (detail.scope, detail.trigger, detail.decideYourself when the person resumed during a hand-back; detail.trigger is restored when the mode came back with the session after a reload, applied once its flow re-activated, which starts nothing by itself); autopilot.carried is the mode, running or paused, going with the task to the agent that took it over, rather than ending with the flow as on any other change of flow owner: to the target of a hand-off, or back to its caller on the hand-back (detail.from, detail.to, detail.mode); a pause the person chose goes with it, and Stop and Clear still end it; a mode restored for the target of a hand-off applies to its caller when the reload hands back; autopilot.error is the wrapper itself failing; autopilot.refinement counts a refining action against its step — a written answer (reply) or a widget action declared refines, such as a regenerate with instructions, counted together (detail.action, detail.stepKey, detail.count, detail.limitReached, after which every refining action is withdrawn for that step: not listed, and refused; the decision and every relisting of its actions say how many are left at the step, as a line Refinements left at this step: N, wherever the widget has a refining action); autopilot.handback-presented is the turn-level hand-back, shown when a working turn ended needing the person with no widget to hand over (detail.why: max-iterations, context-exhausted, failure or question; detail.options; detail.choice once answered); autopilot.catch-up is the autopilot being given the whole visible transcript at once (detail.why: first, for its first decision in a flow or after a reload; size, when its own history passed its budget and was rebuilt short — detail.ownChars; or compacted, when a compaction removed the last message it had seen, so it reads the summary and everything since, short; in each, what it had sent on the user behalf is included and labelled as its own, since its own history does not hold it); autopilot.cost-dropped is autopilot spend that no message carries, so it is NOT in the session total (detail.costUsd and detail.why: handed-back, aborted or stale for a written question it answered that no reply was sent for — stale when a pause, a resume, a new turn or the end of its flow came before the answer landed, which then went nowhere — or no-recorder when the driver had no way to book a decision to its interaction); a hand-back trigger can also be budget (the decision passed its spend cap), watchdog (its work went quiet: no ready surface and no heartbeat, or its own decision stalled with no model call landing, which stops that decision) or detached (the autopilot was removed while it held the interaction, which went to the person with no reason shown). To debug one autopilot decision, follow its interactionId: interaction.requested (heldBy), autopilot.claimed, autopilot.decision-sent, autopilot.acted or autopilot.handed-back, interaction.invoked, interaction.resolved (answeredBy), then the next turn of the working agent), interaction.invoked (a claimant ran one of the actions the widget itself exposes, through its hold, the same code the button runs — detail.claimant, detail.action (an option id, select or reply) and detail.finished, whether that settled the interaction, and when it did not, the run is recorded as an edit; the choice and narration it recorded reach the result of the tool that asked as decidedOnUsersBehalf; the person's own programmatic action, from a host with no DOM to click, carries detail.by: user in place of detail.claimant, and is not recorded as an edit nor as a decision on their behalf, so what it settles is answered by user), interaction.step (a long action run as named steps through runSteps, which survive a reload — detail.step and detail.outcome: started, done, skipped when the step was already done before a reload, or resumed when it started before a reload and runs again with what it saved part-way, such as a job id it re-attaches to), interaction.checkpoint (the checkpoints a conversation keeps for its long actions: the one in flight, and each one a reload cut, held apart until a request of its own widget type takes it over, so a resumed flow that opens other long widgets first, which save their own, loses nothing — detail.action: resumed when a reload cut the work and a request of the same widget type that the registration sameWork agrees is the same work took it over, refused when sameWork said a new request is different work (detail.saved is the identity it kept; detail.kept: true, since it stays for a later request of that type, and is offered to every one until one agrees) or when runSteps found the handed-over checkpoint malformed and started fresh (detail.reason, detail.saved), or cleared when its flow ended, once per checkpoint, with detail.cut: true for one a reload cut that nothing took over), context.updated/threshold-crossed (token + cost), headless.attached (a HeadlessSession attached, for a host that drives the assistant with no screen — detail.label, what the host called itself, when it gave one) and headless.raw-resolved (that host settled an interaction raw, bypassing the widget's own actions: detail.interactionId and detail.component; the result went to the tool as given, so check what the widget would have enforced), context.condensed (a stale tool payload was collapsed out of the model-bound history by a `condenseWhen` declaration on the tool — detail.tool + toolCallId, target args|response, trigger (superseded:<key> or age:<n>), stubLen, and an estimated tokensSaved; stored history and this log keep the FULL payload, so the model-visible slice at any point is the full history minus the condensations recorded up to then), panel.toggled, attachment.added, file.read-failed, suggestions.failed.",
268
273
  'Sub-agent meta events: a sub-agent\'s own turn.retry/turn.error/tool.failed/tool.unresolved events are merged into this same timeline, tagged with `detail.subAgent` — a `"<parent> › <sub-agent>"` breadcrumb that composes when nested (e.g. `"UI Builder › Planner › Grounding"`) — and interleaved by their original timestamps within the subagent.started→completed/failed bracket. These are the per-attempt/per-failure signals that do NOT appear among the sub-agent\'s (hoisted) messages: a malformed/empty attempt that gets retried produces no message, and the stale-vs-hallucinated split and streak counts live only on the event. A sub-agent\'s high-volume, message-derivable events (turn.start/turn.end, provider.selected, context.updated) are intentionally NOT merged — read its hoisted messages for model/tokens/cost and turn-by-turn activity, and the bracketing subagent.* events for the run\'s span.',
269
274
  "`meta` holds context captured at export time: agentSummary (full agent configs), context (active model, contextTokens/contextLimit/contextUsagePercent for the last call, and the session totals — sessionCostUsd, sessionTokensConsumed, and the four-bucket sessionUsage lifted to the top of this log), activeDebugSnapshot (the active agent's `getDebugSnapshot()` taken fresh at export — reflects state NOW, which may have advanced beyond the last turn's agentSnapshot), debug (optional host-supplied debug state), host, and the export timestamp.",
270
275
  'Note the two different scopes in `meta.context`: `contextTokens` is the prompt size of the LAST call (against `contextLimit`, the model context window), while `sessionUsage`/`sessionTokensConsumed` are cumulative BILLED throughput. Every turn resends the conversation, so the cumulative figure counts each turn’s prompt again in the next turn’s and is expected to dwarf the context size — that is not double-counting.',
@@ -42,14 +42,16 @@ const describeViews = (views) => Object.fromEntries(Object.entries(views).map(([
42
42
  const readView = (views, view) => Object.prototype.hasOwnProperty.call(views, view) ? views[view].read() : undefined;
43
43
  /**
44
44
  * Build the surface for a short-form decision (GENC-1660). Its actions are its options (by id),
45
- * plus `select` for a multi-select and `reply` for a written answer; all of them finish. Its views,
46
- * if it has any, are read by name. Every read — facts, views, actions, enabled-ness — is taken
45
+ * plus `select` for a multi-select and `reply` for a written answer; all of them finish. A spec's
46
+ * own `actions` follow them, run as a full-form surface's are (and editing unless they say
47
+ * otherwise); they do not count towards readiness. Its views, if it has any, are read by name. Every read — facts, views, actions, enabled-ness — is taken
47
48
  * fresh, so the surface always describes the widget as it is now.
48
49
  *
49
- * Throws if an option's id collides with another or with a platform-owned name, which is a bug in
50
- * the widget rather than something to recover from.
50
+ * Throws if an option's id collides with another or with a platform-owned name, or an extra action
51
+ * does, which is a bug in the widget rather than something to recover from.
51
52
  */
52
53
  export function buildDecisionSurface(spec) {
54
+ var _a;
53
55
  const options = spec.options.map(normalise);
54
56
  const ids = new Set();
55
57
  for (const { id } of options) {
@@ -60,8 +62,18 @@ export function buildDecisionSurface(spec) {
60
62
  }
61
63
  ids.add(id);
62
64
  }
65
+ const extra = (_a = spec.actions) !== null && _a !== void 0 ? _a : {};
66
+ assertActionShapes('exposeDecision', extra);
67
+ for (const name of Object.keys(extra)) {
68
+ if (ids.has(name) ||
69
+ (spec.multi && name === SELECT_ACTION) ||
70
+ (spec.freeText && name === REPLY_ACTION)) {
71
+ throw new Error(`exposeDecision: the action "${name}" is also an option or a reserved name.`);
72
+ }
73
+ }
63
74
  const byIdOrLabel = (key) => typeof key === 'string' ? options.find((o) => o.id === key || o.label === key) : undefined;
64
- const actions = () => {
75
+ // What the options, `select` and `reply` make of the decision: its readiness is theirs alone.
76
+ const builtIn = () => {
65
77
  const enabled = options.filter(isEnabled);
66
78
  return [
67
79
  ...options.map((o) => {
@@ -81,6 +93,10 @@ export function buildDecisionSurface(spec) {
81
93
  ...(spec.freeText ? [replyAction()] : []),
82
94
  ];
83
95
  };
96
+ const actions = () => [
97
+ ...builtIn(),
98
+ ...Object.entries(extra).map(([name, a]) => describeSpecAction(name, a)),
99
+ ];
84
100
  const replyReason = () => unavailableReason(spec.freeTextUnavailable);
85
101
  const replyAction = () => {
86
102
  const unavailable = replyReason();
@@ -98,7 +114,7 @@ export function buildDecisionSurface(spec) {
98
114
  read: (view) => (view === undefined ? facts() : readView(views, view)),
99
115
  }
100
116
  : {};
101
- return Object.assign(Object.assign({ prompt: spec.prompt, facts }, readable), { actions, isReady: () => actions().some((a) => a.finishes && a.enabled), prepare(name, args) {
117
+ return Object.assign(Object.assign({ prompt: spec.prompt, facts }, readable), { actions, isReady: () => builtIn().some((a) => a.finishes && a.enabled), prepare(name, args) {
102
118
  if (spec.multi && name === SELECT_ACTION) {
103
119
  const picked = args === null || args === void 0 ? void 0 : args.options;
104
120
  if (!Array.isArray(picked) || picked.length === 0) {
@@ -139,6 +155,9 @@ export function buildDecisionSurface(spec) {
139
155
  }
140
156
  const option = options.find((o) => o.id === name);
141
157
  if (!option) {
158
+ const own = ownAction(extra, name);
159
+ if (own)
160
+ return prepareSpecAction(name, own, args);
142
161
  return { error: 'unknown-action', message: `No action "${name}" here.` };
143
162
  }
144
163
  const why = reasonFor(option);
@@ -225,6 +244,52 @@ export function exampleArgs(schema) {
225
244
  const names = Object.keys(properties).sort((a, b) => Number(required.has(b)) - Number(required.has(a)));
226
245
  return Object.fromEntries(names.map((name) => { var _a; return [name, placeholder((_a = properties[name]) === null || _a === void 0 ? void 0 : _a.type)]; }));
227
246
  }
247
+ /** An action as anyone may read it, from a full-form action's spec: one rule for both forms. */
248
+ const describeSpecAction = (name, a) => {
249
+ const unavailable = unavailableReason(a.unavailable);
250
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ name, finishes: !!a.finishes, enabled: unavailable === undefined }, (unavailable !== undefined ? { unavailable } : {})), { description: a.description }), (a.text ? { args: 'text' } : {})), (a.argsSchema ? { args: 'fields', argsSchema: a.argsSchema } : {})), (a.refines ? { refines: true } : {}));
251
+ };
252
+ /** Check a full-form action's guard and arguments, and ready it to run: one rule for both forms. */
253
+ const prepareSpecAction = (name, action, args) => {
254
+ const why = unavailableReason(action.unavailable);
255
+ if (why !== undefined) {
256
+ return { error: 'disabled', message: `"${name}" is not available right now: ${why}.` };
257
+ }
258
+ if (action.text) {
259
+ const text = args === null || args === void 0 ? void 0 : args.text;
260
+ if (typeof text !== 'string' || !text.trim()) {
261
+ return {
262
+ error: 'invalid-args',
263
+ message: `"${name}" takes its argument as \`text\`, not empty.`,
264
+ };
265
+ }
266
+ const trimmed = text.trim();
267
+ return {
268
+ choice: `${name}: ${trimmed}`.slice(0, CHOICE_CHARS),
269
+ finishes: !!action.finishes,
270
+ run: () => action.run(trimmed),
271
+ };
272
+ }
273
+ const problem = checkArgs(args !== null && args !== void 0 ? args : {}, action.argsSchema);
274
+ if (problem)
275
+ return { error: 'invalid-args', message: `"${name}": ${problem}` };
276
+ const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : '';
277
+ return {
278
+ choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
279
+ finishes: !!action.finishes,
280
+ run: () => action.run(args !== null && args !== void 0 ? args : {}),
281
+ };
282
+ };
283
+ /** The action of `actions` called `name`. Own actions only: `constructor` must not reach the prototype. */
284
+ const ownAction = (actions, name) => Object.prototype.hasOwnProperty.call(actions, name) ? actions[name] : undefined;
285
+ /** Refuse an action that takes `text` and `argsSchema` both: a bug in the widget. */
286
+ const assertActionShapes = (who, actions) => {
287
+ for (const [name, action] of Object.entries(actions)) {
288
+ if (action.text && action.argsSchema) {
289
+ throw new Error(`${who}: action "${name}" takes \`text\` or \`argsSchema\`, not both.`);
290
+ }
291
+ }
292
+ };
228
293
  /**
229
294
  * Build the surface for a full-form spec (GENC-1660 §4.2): named actions, some editing and some
230
295
  * finishing, each guarded and checked (a text action's text, a form's fields against its schema); a
@@ -234,59 +299,21 @@ export function exampleArgs(schema) {
234
299
  * views: a bug in the widget rather than something to recover from.
235
300
  */
236
301
  export function buildFullSurface(spec) {
237
- for (const [name, action] of Object.entries(spec.actions)) {
238
- if (action.text && action.argsSchema) {
239
- throw new Error(`exposeSurface: action "${name}" takes \`text\` or \`argsSchema\`, not both.`);
240
- }
241
- }
302
+ assertActionShapes('exposeSurface', spec.actions);
242
303
  const views = spec.views && Object.keys(spec.views).length ? spec.views : undefined;
243
304
  if (spec.preview !== undefined &&
244
305
  !(views && Object.prototype.hasOwnProperty.call(views, spec.preview))) {
245
306
  throw new Error(`exposeSurface: the preview "${spec.preview}" is not one of its views.`);
246
307
  }
247
- const actions = () => Object.entries(spec.actions).map(([name, a]) => {
248
- const unavailable = unavailableReason(a.unavailable);
249
- return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ name, finishes: !!a.finishes, enabled: unavailable === undefined }, (unavailable !== undefined ? { unavailable } : {})), { description: a.description }), (a.text ? { args: 'text' } : {})), (a.argsSchema ? { args: 'fields', argsSchema: a.argsSchema } : {})), (a.refines ? { refines: true } : {}));
250
- });
308
+ const actions = () => Object.entries(spec.actions).map(([name, a]) => describeSpecAction(name, a));
251
309
  return Object.assign(Object.assign({ prompt: spec.prompt, facts: () => ({}), describe: () => spec.describe() }, (views
252
310
  ? Object.assign(Object.assign({ views: describeViews(views) }, (spec.preview !== undefined ? { preview: spec.preview } : {})), { read: (view) => {
253
311
  const name = view !== null && view !== void 0 ? view : spec.preview;
254
312
  return name === undefined ? undefined : readView(views, name);
255
313
  } }) : {})), { actions, isReady: () => actions().some((a) => a.enabled), prepare(name, args) {
256
- // Own actions only: a name from the model such as `constructor` or `toString` must not reach
257
- // the object's prototype.
258
- const action = Object.prototype.hasOwnProperty.call(spec.actions, name)
259
- ? spec.actions[name]
260
- : undefined;
314
+ const action = ownAction(spec.actions, name);
261
315
  if (!action)
262
316
  return { error: 'unknown-action', message: `No action "${name}" here.` };
263
- const why = unavailableReason(action.unavailable);
264
- if (why !== undefined) {
265
- return { error: 'disabled', message: `"${name}" is not available right now: ${why}.` };
266
- }
267
- if (action.text) {
268
- const text = args === null || args === void 0 ? void 0 : args.text;
269
- if (typeof text !== 'string' || !text.trim()) {
270
- return {
271
- error: 'invalid-args',
272
- message: `"${name}" takes its argument as \`text\`, not empty.`,
273
- };
274
- }
275
- const trimmed = text.trim();
276
- return {
277
- choice: `${name}: ${trimmed}`.slice(0, CHOICE_CHARS),
278
- finishes: !!action.finishes,
279
- run: () => action.run(trimmed),
280
- };
281
- }
282
- const problem = checkArgs(args !== null && args !== void 0 ? args : {}, action.argsSchema);
283
- if (problem)
284
- return { error: 'invalid-args', message: `"${name}": ${problem}` };
285
- const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : '';
286
- return {
287
- choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
288
- finishes: !!action.finishes,
289
- run: () => action.run(args !== null && args !== void 0 ? args : {}),
290
- };
317
+ return prepareSpecAction(name, action, args);
291
318
  } });
292
319
  }
@@ -0,0 +1,69 @@
1
+ import { __awaiter } from "tslib";
2
+ import { logger } from '../../utils/logger';
3
+ import { DEBUG_LOG_README, getMetaEvents } from '../debug-event-log';
4
+ import { buildTimelineEntries } from './build-timeline-entries';
5
+ import { assembleDebugLog, withFreshMetaSnapshot, } from './diagnostics';
6
+ /**
7
+ * The current page's diagnostic entries (GENC-1351 §5.8): the debug-log timeline entries
8
+ * (`message`/`turn`/`event`), the driver's external diagnostics, and one `meta-snapshot` carrying
9
+ * the export-time `meta` block. The assistant element reassembles these into the live log, and the
10
+ * persister's forward-capture path diffs them against what has already been appended to the stored
11
+ * stream. Extracted from the element so a host with no element (the headless session) derives its
12
+ * log from the same code, not a second copy that could drift.
13
+ *
14
+ * DOM-free and framework-free.
15
+ *
16
+ * @beta
17
+ */
18
+ export function buildDiagnosticEntries(input) {
19
+ var _a, _b, _c, _d, _e, _f, _g;
20
+ const { driver, stateKey } = input;
21
+ // The message/turn/event timeline entries — built by the shared, pure `buildTimelineEntries`
22
+ // (the same helper a headless consumer uses to harvest its own log), from the driver's pull
23
+ // surfaces. Prefer the driver's raw history (carries sub-agent traces) over the host's copy.
24
+ const timelineEntries = buildTimelineEntries({
25
+ turnSnapshots: (_b = (_a = driver === null || driver === void 0 ? void 0 : driver.getTurnSnapshots) === null || _a === void 0 ? void 0 : _a.call(driver)) !== null && _b !== void 0 ? _b : [],
26
+ messages: (_e = (_d = (_c = driver === null || driver === void 0 ? void 0 : driver.getRawHistory) === null || _c === void 0 ? void 0 : _c.call(driver)) !== null && _d !== void 0 ? _d : input.messages) !== null && _e !== void 0 ? _e : [],
27
+ metaEvents: stateKey ? getMetaEvents(stateKey) : [],
28
+ });
29
+ // Fold in any external diagnostics harvested from an out-of-band driver (e.g. a server-side
30
+ // ChatDriver whose collated log an interaction widget returned on its result). They ride the
31
+ // same download + persisted-diagnostics path; `assembleDebugLog` sorts the whole timeline by
32
+ // timestamp so they interleave chronologically. (GENC-1461 unified diagnostics.)
33
+ return [
34
+ ...timelineEntries,
35
+ ...((_g = (_f = driver === null || driver === void 0 ? void 0 : driver.getExternalDiagnostics) === null || _f === void 0 ? void 0 : _f.call(driver)) !== null && _g !== void 0 ? _g : []),
36
+ input.metaSnapshot(),
37
+ ];
38
+ }
39
+ /**
40
+ * The debug log for the WHOLE session (GENC-1351 §5.8 / §5.10), not just this page's activity:
41
+ * the persisted diagnostics stream with its stored `meta-snapshot`s replaced by a fresh one
42
+ * (`withFreshMetaSnapshot`), so its context and cost figures are current. When the provider
43
+ * stores no diagnostics, there is no key, the stream is empty or loading it fails, it is
44
+ * `fallback()` — the current-page log. Never throws.
45
+ *
46
+ * The one derivation of the lifetime log: the assistant element's `getLifetimeDebugLog` and the
47
+ * headless session both call it, because a second one drifts, and for this log that means silently
48
+ * understating spend.
49
+ *
50
+ * @beta
51
+ */
52
+ export function lifetimeDebugLog(input) {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ const { provider, stateKey } = input;
55
+ if ((provider === null || provider === void 0 ? void 0 : provider.loadDiagnostics) && stateKey) {
56
+ try {
57
+ yield input.flush();
58
+ const stored = yield provider.loadDiagnostics(stateKey);
59
+ if (stored.length) {
60
+ return assembleDebugLog(withFreshMetaSnapshot(stored, input.metaSnapshot()), DEBUG_LOG_README);
61
+ }
62
+ }
63
+ catch (e) {
64
+ logger.error('Diagnostics load failed — using current-page log:', e);
65
+ }
66
+ }
67
+ return input.fallback();
68
+ });
69
+ }
@@ -1,3 +1,4 @@
1
1
  export { PERSISTED_SESSION_VERSION } from './session-snapshot';
2
2
  export { WebStorageSessionProvider } from './session-persistence-provider';
3
3
  export { assembleDebugLog, withFreshMetaSnapshot } from './diagnostics';
4
+ export { buildDiagnosticEntries, lifetimeDebugLog } from './debug-log-assembly';
@@ -213,6 +213,22 @@ export class SessionPersister {
213
213
  void this.persistSession();
214
214
  }, delay);
215
215
  }
216
+ /**
217
+ * Save the session now, and wait for the write: the pending debounced autosave, if there is one,
218
+ * is taken over by this call. For a host with no `pagehide` to save on, which wants the snapshot
219
+ * written before it reads it back or exits (the headless session's `flush`). Like the autosave it
220
+ * replaces, it never throws: a failed write is recorded and, repeated, reported.
221
+ */
222
+ saveNow() {
223
+ return __awaiter(this, void 0, void 0, function* () {
224
+ const { provider, enabled } = this.deps.getPersistence();
225
+ if (!enabled || !provider)
226
+ return;
227
+ this.cancelPendingSave();
228
+ this._lastSaveAt = Date.now();
229
+ yield this.persistSession();
230
+ });
231
+ }
216
232
  /**
217
233
  * Cancel a pending debounced autosave **without flushing it** (GENC-1351 §5.12).
218
234
  * Called before a session switch/re-key so the outgoing transcript can never be