@genesislcap/ai-assistant 15.2.1 → 15.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +191 -191
- package/dist/dts/react.d.ts +11 -11
- package/dist/dts/state/persistence/diagnostics.d.ts +9 -1
- package/dist/dts/state/persistence/diagnostics.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts +12 -0
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/esm/state/persistence/diagnostics.js +11 -3
- package/dist/esm/state/persistence/diagnostics.test.js +36 -1
- package/dist/esm/state/persistence/session-persister.js +12 -0
- package/dist/esm/state/persistence/session-persister.test.js +25 -0
- package/dist/react.cjs +11 -11
- package/dist/react.mjs +10 -10
- package/package.json +17 -17
- package/src/state/persistence/diagnostics.test.ts +45 -1
- package/src/state/persistence/diagnostics.ts +10 -2
- package/src/state/persistence/session-persister.test.ts +44 -0
- package/src/state/persistence/session-persister.ts +12 -0
package/dist/react.cjs
CHANGED
|
@@ -102,6 +102,16 @@ const AiChatBubble = React.forwardRef(function AiChatBubble(props, ref) {
|
|
|
102
102
|
return React.createElement(customElements.getName(AiChatBubbleWC) ?? 'foundation-ai-chat-bubble', { ...rest, ref }, children);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
+
const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
106
|
+
const { children, ...rest } = props;
|
|
107
|
+
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
111
|
+
const { children, ...rest } = props;
|
|
112
|
+
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
113
|
+
});
|
|
114
|
+
|
|
105
115
|
const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrapper(props, ref) {
|
|
106
116
|
const { onRequestScrollToBottom, onInteractionCompleted, children, ...rest } = props;
|
|
107
117
|
const _innerRef = React.useRef(null);
|
|
@@ -124,16 +134,6 @@ const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrap
|
|
|
124
134
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
|
125
135
|
});
|
|
126
136
|
|
|
127
|
-
const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
128
|
-
const { children, ...rest } = props;
|
|
129
|
-
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
133
|
-
const { children, ...rest } = props;
|
|
134
|
-
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
137
|
module.exports = {
|
|
138
138
|
AiFlowingWavesIndicator,
|
|
139
139
|
AiHaloOverlay,
|
|
@@ -144,7 +144,7 @@ module.exports = {
|
|
|
144
144
|
AiActivityHalo,
|
|
145
145
|
AgentPicker,
|
|
146
146
|
AiChatBubble,
|
|
147
|
-
AiChatInteractionWrapper,
|
|
148
147
|
AiChatMarkdown,
|
|
149
148
|
FoundationAiPopoutManager,
|
|
149
|
+
AiChatInteractionWrapper,
|
|
150
150
|
};
|
package/dist/react.mjs
CHANGED
|
@@ -100,6 +100,16 @@ export const AiChatBubble = React.forwardRef(function AiChatBubble(props, ref) {
|
|
|
100
100
|
return React.createElement(customElements.getName(AiChatBubbleWC) ?? 'foundation-ai-chat-bubble', { ...rest, ref }, children);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
export const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
104
|
+
const { children, ...rest } = props;
|
|
105
|
+
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
109
|
+
const { children, ...rest } = props;
|
|
110
|
+
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
111
|
+
});
|
|
112
|
+
|
|
103
113
|
export const AiChatInteractionWrapper = React.forwardRef(function AiChatInteractionWrapper(props, ref) {
|
|
104
114
|
const { onRequestScrollToBottom, onInteractionCompleted, children, ...rest } = props;
|
|
105
115
|
const _innerRef = React.useRef(null);
|
|
@@ -121,13 +131,3 @@ export const AiChatInteractionWrapper = React.forwardRef(function AiChatInteract
|
|
|
121
131
|
}, []);
|
|
122
132
|
return React.createElement(customElements.getName(AiChatInteractionWrapperWC) ?? 'ai-chat-interaction-wrapper', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
|
|
123
133
|
});
|
|
124
|
-
|
|
125
|
-
export const AiChatMarkdown = React.forwardRef(function AiChatMarkdown(props, ref) {
|
|
126
|
-
const { children, ...rest } = props;
|
|
127
|
-
return React.createElement(customElements.getName(AiChatMarkdownWC) ?? 'ai-chat-markdown', { ...rest, ref }, children);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
export const FoundationAiPopoutManager = React.forwardRef(function FoundationAiPopoutManager(props, ref) {
|
|
131
|
-
const { children, ...rest } = props;
|
|
132
|
-
return React.createElement(customElements.getName(FoundationAiPopoutManagerWC) ?? 'foundation-ai-popout-manager', { ...rest, ref }, children);
|
|
133
|
-
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genesislcap/ai-assistant",
|
|
3
3
|
"description": "Genesis AI Assistant micro-frontend",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.3.1",
|
|
5
5
|
"license": "SEE LICENSE IN license.txt",
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/ai-assistant.d.ts",
|
|
@@ -73,26 +73,26 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@genesislcap/foundation-testing": "15.
|
|
77
|
-
"@genesislcap/genx": "15.
|
|
78
|
-
"@genesislcap/rollup-builder": "15.
|
|
79
|
-
"@genesislcap/ts-builder": "15.
|
|
80
|
-
"@genesislcap/uvu-playwright-builder": "15.
|
|
81
|
-
"@genesislcap/vite-builder": "15.
|
|
82
|
-
"@genesislcap/webpack-builder": "15.
|
|
76
|
+
"@genesislcap/foundation-testing": "15.3.1",
|
|
77
|
+
"@genesislcap/genx": "15.3.1",
|
|
78
|
+
"@genesislcap/rollup-builder": "15.3.1",
|
|
79
|
+
"@genesislcap/ts-builder": "15.3.1",
|
|
80
|
+
"@genesislcap/uvu-playwright-builder": "15.3.1",
|
|
81
|
+
"@genesislcap/vite-builder": "15.3.1",
|
|
82
|
+
"@genesislcap/webpack-builder": "15.3.1",
|
|
83
83
|
"@types/dompurify": "^3.0.5",
|
|
84
84
|
"@types/marked": "^5.0.2",
|
|
85
85
|
"esbuild": "0.25.12"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@genesislcap/foundation-ai": "15.
|
|
89
|
-
"@genesislcap/foundation-logger": "15.
|
|
90
|
-
"@genesislcap/foundation-notifications": "15.
|
|
91
|
-
"@genesislcap/foundation-redux": "15.
|
|
92
|
-
"@genesislcap/foundation-ui": "15.
|
|
93
|
-
"@genesislcap/foundation-utils": "15.
|
|
94
|
-
"@genesislcap/rapid-design-system": "15.
|
|
95
|
-
"@genesislcap/web-core": "15.
|
|
88
|
+
"@genesislcap/foundation-ai": "15.3.1",
|
|
89
|
+
"@genesislcap/foundation-logger": "15.3.1",
|
|
90
|
+
"@genesislcap/foundation-notifications": "15.3.1",
|
|
91
|
+
"@genesislcap/foundation-redux": "15.3.1",
|
|
92
|
+
"@genesislcap/foundation-ui": "15.3.1",
|
|
93
|
+
"@genesislcap/foundation-utils": "15.3.1",
|
|
94
|
+
"@genesislcap/rapid-design-system": "15.3.1",
|
|
95
|
+
"@genesislcap/web-core": "15.3.1",
|
|
96
96
|
"dompurify": "^3.3.1",
|
|
97
97
|
"marked": "^17.0.3"
|
|
98
98
|
},
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"access": "public"
|
|
106
106
|
},
|
|
107
107
|
"customElements": "dist/custom-elements.json",
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "1c3a33f7d56acec166f832b9610e83343a8873e6"
|
|
109
109
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
2
|
-
import { assembleDebugLog, type DiagnosticEntry } from './diagnostics';
|
|
2
|
+
import { assembleDebugLog, diagnosticsMessageKey, type DiagnosticEntry } from './diagnostics';
|
|
3
3
|
|
|
4
4
|
// GENC-1351 §5.8: `assembleDebugLog` reassembles the forward-only diagnostics
|
|
5
5
|
// stream back into the `{ readme, timeline, meta }` debug-log shape — timeline
|
|
@@ -93,3 +93,47 @@ Suite('collapses exact-duplicate entries but keeps genuinely-distinct ones', ()
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
Suite.run();
|
|
96
|
+
|
|
97
|
+
// GENC-1461: the reasoning/narration/answer split (GENC-1411) emits several messages from ONE model
|
|
98
|
+
// response, so they share `timestamp` + `role` + `subAgentOf`. The message identity key must still
|
|
99
|
+
// tell them apart — otherwise the forward-capture dedup treats them as one and drops all but the
|
|
100
|
+
// first (a server generation's answer message vanished from the persisted download while its
|
|
101
|
+
// reasoning survived).
|
|
102
|
+
const KeySuite = createLogicSuite('diagnosticsMessageKey');
|
|
103
|
+
|
|
104
|
+
KeySuite('gives co-timestamped split messages (reasoning vs answer) distinct keys', () => {
|
|
105
|
+
const base = {
|
|
106
|
+
kind: 'message' as const,
|
|
107
|
+
role: 'assistant',
|
|
108
|
+
timestamp: '2026-01-01T00:00:05.000Z',
|
|
109
|
+
};
|
|
110
|
+
const reasoning: DiagnosticEntry = { ...base, category: 'reasoning', content: 'thinking…' };
|
|
111
|
+
const answer: DiagnosticEntry = { ...base, content: 'the generated code' }; // no category = the answer
|
|
112
|
+
const kReasoning = diagnosticsMessageKey(reasoning);
|
|
113
|
+
const kAnswer = diagnosticsMessageKey(answer);
|
|
114
|
+
assert.ok(kReasoning, 'reasoning message has a key');
|
|
115
|
+
assert.ok(kAnswer, 'answer message has a key');
|
|
116
|
+
assert.not.equal(
|
|
117
|
+
kReasoning,
|
|
118
|
+
kAnswer,
|
|
119
|
+
'a reasoning and an answer split from one response must not collide',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
KeySuite('still collapses a genuine re-append of the same message (same key)', () => {
|
|
124
|
+
const m: DiagnosticEntry = {
|
|
125
|
+
kind: 'message',
|
|
126
|
+
role: 'assistant',
|
|
127
|
+
timestamp: '2026-01-01T00:00:05.000Z',
|
|
128
|
+
category: 'reasoning',
|
|
129
|
+
content: 'thinking…',
|
|
130
|
+
};
|
|
131
|
+
// Two instances / a reload re-appending the SAME message must still share one key.
|
|
132
|
+
assert.equal(diagnosticsMessageKey(m), diagnosticsMessageKey({ ...m }));
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
KeySuite('returns null when the entry has no timestamp (treated as always-new)', () => {
|
|
136
|
+
assert.is(diagnosticsMessageKey({ kind: 'message', role: 'assistant' }), null);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
KeySuite.run();
|
|
@@ -42,18 +42,26 @@ export interface DebugLog {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Stable identity for a message diagnostic entry — `timestamp::role::subAgentOf`.
|
|
45
|
+
* Stable identity for a message diagnostic entry — `timestamp::role::subAgentOf::category`.
|
|
46
46
|
* Used by the forward-capture delta (skip already-emitted messages) and by the
|
|
47
47
|
* restore watermark (skip messages already present in a prior load's stream).
|
|
48
48
|
* `null` when the entry has no timestamp → treat as always-new. Shared so the
|
|
49
49
|
* element's diagnostics capture and the `SessionPersister`'s restore watermark
|
|
50
50
|
* compute identity identically (GENC-1351 §5.8/§5.10).
|
|
51
51
|
*
|
|
52
|
+
* `category` is part of the key because the reasoning/narration/answer split (GENC-1411) emits
|
|
53
|
+
* SEVERAL messages from one model response — the thought summary, any interstitial narration, and
|
|
54
|
+
* the answer/tool-call message — which share `timestamp` + `role` + `subAgentOf`. Keying on those
|
|
55
|
+
* alone collapsed them to one, so the forward-capture dropped all but the first: a server
|
|
56
|
+
* generation's answer message vanished from the persisted debug log while its reasoning survived
|
|
57
|
+
* (GENC-1461). `category` is stable across a message's in-place annotation (only `toolCalls` change,
|
|
58
|
+
* which is deliberately excluded), so a genuine re-append of the same message still dedups.
|
|
59
|
+
*
|
|
52
60
|
* @internal
|
|
53
61
|
*/
|
|
54
62
|
export function diagnosticsMessageKey(entry: DiagnosticEntry): string | null {
|
|
55
63
|
return typeof entry.timestamp === 'string'
|
|
56
|
-
? `${entry.timestamp}::${(entry.role as string) ?? ''}::${(entry.subAgentOf as string) ?? ''}`
|
|
64
|
+
? `${entry.timestamp}::${(entry.role as string) ?? ''}::${(entry.subAgentOf as string) ?? ''}::${(entry.category as string) ?? ''}`
|
|
57
65
|
: null;
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -367,6 +367,16 @@ const metaSnap = (dedupSignature: string, meta: unknown): DiagnosticEntry => ({
|
|
|
367
367
|
dedupSignature,
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
+
// A diagnostic MESSAGE entry as the reasoning/narration/answer split (GENC-1411) produces them —
|
|
371
|
+
// several messages from ONE response, sharing timestamp + role, differing by `category` / content.
|
|
372
|
+
const diagMsg = (over: Partial<DiagnosticEntry> = {}): DiagnosticEntry => ({
|
|
373
|
+
kind: 'message',
|
|
374
|
+
role: 'assistant',
|
|
375
|
+
timestamp: '2026-01-01T00:00:05.000Z',
|
|
376
|
+
content: '',
|
|
377
|
+
...over,
|
|
378
|
+
});
|
|
379
|
+
|
|
370
380
|
Suite('scheduleDiagnostics() appends the collected delta once per window', async () => {
|
|
371
381
|
const { p, f } = makePersister({ diagnosticEntries: [evt(0), evt(1)] });
|
|
372
382
|
p.scheduleDiagnostics();
|
|
@@ -425,4 +435,38 @@ Suite(
|
|
|
425
435
|
},
|
|
426
436
|
);
|
|
427
437
|
|
|
438
|
+
Suite(
|
|
439
|
+
'flushDiagnostics() keeps co-timestamped split messages (reasoning + answer), not deduped as one',
|
|
440
|
+
async () => {
|
|
441
|
+
// GENC-1461: the reasoning/narration/answer split emits several messages from ONE response, so
|
|
442
|
+
// they share timestamp + role + subAgentOf. The forward-capture keyed only on those, so it
|
|
443
|
+
// emitted the reasoning message and dropped the answer as a "duplicate" — a server generation's
|
|
444
|
+
// answer message vanished from the persisted download while its reasoning survived. Both must be
|
|
445
|
+
// captured.
|
|
446
|
+
const reasoning = diagMsg({ category: 'reasoning', content: 'thinking…' });
|
|
447
|
+
const answer = diagMsg({ content: 'the generated code' }); // no category = the answer/tool-call slot
|
|
448
|
+
const { p, f } = makePersister({ diagnosticEntries: [reasoning, answer] });
|
|
449
|
+
await p.flushDiagnostics();
|
|
450
|
+
assert.is(f.appended.length, 1, 'one flush');
|
|
451
|
+
assert.is(
|
|
452
|
+
f.appended[0].length,
|
|
453
|
+
2,
|
|
454
|
+
'both the reasoning and the answer message are captured, not collapsed to one',
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
Suite(
|
|
460
|
+
'flushDiagnostics() still dedups a genuine re-append of the same message across flushes',
|
|
461
|
+
async () => {
|
|
462
|
+
// The dedup must still stop the SAME message being re-appended on every throttled flush.
|
|
463
|
+
const m = diagMsg({ category: 'reasoning', content: 'thinking…' });
|
|
464
|
+
const { p, f } = makePersister({ diagnosticEntries: [m] });
|
|
465
|
+
await p.flushDiagnostics();
|
|
466
|
+
assert.is(f.appended.length, 1, 'first flush appends the message');
|
|
467
|
+
await p.flushDiagnostics();
|
|
468
|
+
assert.is(f.appended.length, 1, 'unchanged message → no re-append');
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
|
|
428
472
|
Suite.run();
|
|
@@ -638,6 +638,18 @@ export class SessionPersister {
|
|
|
638
638
|
* (§5.8). Skips messages present at restore or already appended; emits each turn
|
|
639
639
|
* (by turn-index) and meta-event (by index) once; appends a `meta-snapshot` only
|
|
640
640
|
* when the block changed. Advances the shared per-session cursors as it goes.
|
|
641
|
+
*
|
|
642
|
+
* TODO(GENC-1461, longer-term): this whole delta step exists only because the element *pulls* the
|
|
643
|
+
* ENTIRE debug log on every throttled flush (`getDiagnosticEntries()` returns the full timeline),
|
|
644
|
+
* so we must diff it against per-session cursors to append only the new tail — and messages, unlike
|
|
645
|
+
* events (monotonic `index`) and turns (`turnIndex`), have no stable positional cursor (compaction
|
|
646
|
+
* rewrites ranges, tool-call messages are annotated in place, synthetic messages are spliced in,
|
|
647
|
+
* restore reloads them), forcing a content-derived identity (`diagnosticsMessageKey`). That
|
|
648
|
+
* identity is inherently fragile: it was too coarse for the reasoning/answer/tool-call split and
|
|
649
|
+
* silently dropped co-timestamped messages (GENC-1461; fixed for now by adding `category`). The
|
|
650
|
+
* robust fix is to invert the flow — have the driver *push* each diagnostic entry exactly once at
|
|
651
|
+
* creation (an append-only event stream) instead of the element pulling-and-diffing — which removes
|
|
652
|
+
* the message-identity key and this dedup entirely. Bigger change; deferred.
|
|
641
653
|
*/
|
|
642
654
|
private collectDiagnosticsDelta(): DiagnosticEntry[] {
|
|
643
655
|
const cursors = getDiagnosticsCursors(this.stateKey);
|