@ai-matrx/chat 0.7.1 → 0.7.2
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/CHANGELOG.md +6 -0
- package/dist/agents/redux/execution-system/instance-resources/default-remark-durability.d.ts +8 -2
- package/dist/agents/redux/execution-system/instance-resources/default-remark-durability.js +61 -18
- package/dist/agents/redux/execution-system/instance-resources/remark-feed.d.ts +26 -0
- package/dist/agents/redux/execution-system/instance-resources/remark-feed.js +87 -0
- package/dist/agents/redux/execution-system/instance-resources/remarks.d.ts +10 -0
- package/dist/agents/redux/execution-system/instance-resources/remarks.js +11 -3
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.d.ts +65 -335
- package/dist/agents/redux/execution-system/utils/stream-block-accumulator.js +224 -1909
- package/dist/host/react.js +5 -3
- package/dist/window-panels/windows/agents/AgentRunWindow.js +3 -3
- package/package.json +8 -8
- package/dist/agents/redux/execution-system/utils/content-prefilter.d.ts +0 -50
- package/dist/agents/redux/execution-system/utils/content-prefilter.js +0 -243
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/chat/v0.7.1..npm/chat/v0.7.2 -- apps/shared/chat`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
3
9
|
## 0.7.1
|
|
4
10
|
|
|
5
11
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
package/dist/agents/redux/execution-system/instance-resources/default-remark-durability.d.ts
CHANGED
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
* (state_version > sent_version, not dismissed at that version) and carries `blockStateRef` so the
|
|
9
9
|
* server marks it sent with the message. A chip belongs to the conversation it was staged into.
|
|
10
10
|
* Registered by `<ChatProvider>` for a private store when the host registered no port of its own.
|
|
11
|
-
*
|
|
11
|
+
* Live: a chip made, sent or dismissed in another tab or on another device lands here without a
|
|
12
|
+
* reload, through the realtime feed (`subscribeFeed`, injectable for tests). Runs on EVERY host.
|
|
12
13
|
*/
|
|
13
14
|
import type { ChatDb } from "../../../../host/contract.js";
|
|
14
15
|
import type { ChatDispatch } from "../../../../store/root-state.js";
|
|
16
|
+
import { type RemarkFeedSubscribe } from "./remark-feed.js";
|
|
15
17
|
import { type RemarkDurability } from "./remarks.js";
|
|
16
18
|
export interface DefaultRemarkDurabilityOptions {
|
|
17
19
|
db: ChatDb;
|
|
18
20
|
dispatch: ChatDispatch;
|
|
21
|
+
/** The host store's state: answers (a conversation with none has no feed), sent chips, the person. */
|
|
22
|
+
getState: () => unknown;
|
|
23
|
+
/** The live feed of a conversation's rows. Default: the realtime broadcast. */
|
|
24
|
+
subscribeFeed?: RemarkFeedSubscribe;
|
|
19
25
|
/** A write that failed: say so (the host's notify + console). */
|
|
20
26
|
onFailure: (message: string, error: unknown) => void;
|
|
21
27
|
}
|
|
22
|
-
export declare function createDbRemarkDurability({ db, dispatch, onFailure }: DefaultRemarkDurabilityOptions): RemarkDurability;
|
|
28
|
+
export declare function createDbRemarkDurability({ db, dispatch, getState, subscribeFeed, onFailure }: DefaultRemarkDurabilityOptions): RemarkDurability;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { durableRecordId } from "@ai-matrx/kit/ids";
|
|
2
|
+
import { selectUserId } from "../../../../host/identity.js";
|
|
3
|
+
import { subscribeRemarkFeed } from "./remark-feed.js";
|
|
2
4
|
import {
|
|
3
5
|
attachRemarkRef,
|
|
4
6
|
readStoredRemarks,
|
|
7
|
+
remarkSourceOf,
|
|
5
8
|
restageRemarks,
|
|
6
9
|
unstageRemark
|
|
7
10
|
} from "./remarks.js";
|
|
@@ -35,7 +38,7 @@ function stripRef(item) {
|
|
|
35
38
|
const { blockStateRef: _ref, ...rest } = item;
|
|
36
39
|
return rest;
|
|
37
40
|
}
|
|
38
|
-
function createDbRemarkDurability({ db, dispatch, onFailure }) {
|
|
41
|
+
function createDbRemarkDurability({ db, dispatch, getState, subscribeFeed = subscribeRemarkFeed, onFailure }) {
|
|
39
42
|
const rpc = (name, args) => db.rpc(name, args);
|
|
40
43
|
const slots = /* @__PURE__ */ new Map();
|
|
41
44
|
const restored = /* @__PURE__ */ new Set();
|
|
@@ -87,6 +90,52 @@ function createDbRemarkDurability({ db, dispatch, onFailure }) {
|
|
|
87
90
|
}
|
|
88
91
|
});
|
|
89
92
|
};
|
|
93
|
+
const applyRows = (conversationId, all) => {
|
|
94
|
+
const mine = all.filter((r) => r.kind === "remark" && r.state.stagedIn === conversationId && r.state.remark && typeof r.state.remark === "object");
|
|
95
|
+
const state = getState();
|
|
96
|
+
const sentRefs = /* @__PURE__ */ new Set();
|
|
97
|
+
for (const resourceId of state.instanceResources?.submittedIds?.[conversationId] ?? []) {
|
|
98
|
+
const resource = state.instanceResources?.byConversationId?.[conversationId]?.[resourceId];
|
|
99
|
+
const sentRef = resource ? remarkSourceOf(resource)?.remark.blockStateRef : null;
|
|
100
|
+
if (sentRef) sentRefs.add(`${sentRef.id}:${sentRef.stateVersion}`);
|
|
101
|
+
}
|
|
102
|
+
for (const r of mine) {
|
|
103
|
+
if (!isDue(r) && typeof r.state.coalesceKey === "string") {
|
|
104
|
+
dispatch(unstageRemark(conversationId, r.state.coalesceKey, { retire: false }));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const stored = readStoredRemarks(
|
|
108
|
+
mine.filter(isDue).filter((r) => !sentRefs.has(`${r.id}:${r.state_version}`)).map((r) => ({
|
|
109
|
+
resourceId: typeof r.state.resourceId === "string" ? r.state.resourceId : r.id,
|
|
110
|
+
coalesceKey: typeof r.state.coalesceKey === "string" ? r.state.coalesceKey : null,
|
|
111
|
+
item: { ...r.state.remark, blockStateRef: { id: r.id, stateVersion: r.state_version } }
|
|
112
|
+
}))
|
|
113
|
+
);
|
|
114
|
+
if (stored.length > 0) dispatch(restageRemarks(conversationId, stored));
|
|
115
|
+
};
|
|
116
|
+
const watching = /* @__PURE__ */ new Map();
|
|
117
|
+
const watch = (conversationId) => {
|
|
118
|
+
if (watching.has(conversationId)) return;
|
|
119
|
+
const answers = getState().messages?.byConversationId?.[conversationId]?.orderedIds?.length ?? 0;
|
|
120
|
+
if (answers === 0) return;
|
|
121
|
+
const userId = selectUserId(getState());
|
|
122
|
+
if (!userId) return;
|
|
123
|
+
const release = subscribeFeed({ scope: "conversation", entityId: conversationId }, userId, (signal) => {
|
|
124
|
+
if ("rows" in signal) {
|
|
125
|
+
applyRows(conversationId, signal.rows);
|
|
126
|
+
} else {
|
|
127
|
+
void (async () => {
|
|
128
|
+
const { data, error } = await rpc("block_state_list_staged", { p_conversation_id: conversationId });
|
|
129
|
+
if (error) return console.error("[remarks] resync failed:", error.message);
|
|
130
|
+
applyRows(conversationId, (Array.isArray(data) ? data : []).flatMap((e) => {
|
|
131
|
+
const r = rowOf(e);
|
|
132
|
+
return r ? [r] : [];
|
|
133
|
+
}));
|
|
134
|
+
})();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
watching.set(conversationId, release);
|
|
138
|
+
};
|
|
90
139
|
return {
|
|
91
140
|
save(conversationId, resourceId, coalesceKey, item) {
|
|
92
141
|
const key = coalesceKey ?? resourceId;
|
|
@@ -125,36 +174,30 @@ function createDbRemarkDurability({ db, dispatch, onFailure }) {
|
|
|
125
174
|
await Promise.all(slotsOf(conversationId).map(([, slot]) => slot.chain));
|
|
126
175
|
},
|
|
127
176
|
restore(conversationId) {
|
|
128
|
-
if (restored.has(conversationId)
|
|
177
|
+
if (restored.has(conversationId)) {
|
|
178
|
+
watch(conversationId);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (restoring.has(conversationId)) return;
|
|
129
182
|
restoring.add(conversationId);
|
|
130
183
|
void (async () => {
|
|
184
|
+
let rows;
|
|
131
185
|
try {
|
|
132
186
|
const { data, error } = await rpc("block_state_list_staged", { p_conversation_id: conversationId });
|
|
133
187
|
if (error) throw new Error(error.message);
|
|
134
|
-
|
|
135
|
-
const rows = (Array.isArray(data) ? data : []).flatMap((entry) => {
|
|
188
|
+
rows = (Array.isArray(data) ? data : []).flatMap((entry) => {
|
|
136
189
|
const row = rowOf(entry);
|
|
137
190
|
return row ? [row] : [];
|
|
138
191
|
});
|
|
139
|
-
const mine = rows.filter((r) => r.kind === "remark" && r.state.stagedIn === conversationId && r.state.remark && typeof r.state.remark === "object");
|
|
140
|
-
for (const r of mine) {
|
|
141
|
-
if (!isDue(r) && typeof r.state.coalesceKey === "string") {
|
|
142
|
-
dispatch(unstageRemark(conversationId, r.state.coalesceKey, { retire: false }));
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const stored = readStoredRemarks(
|
|
146
|
-
mine.filter(isDue).map((r) => ({
|
|
147
|
-
resourceId: typeof r.state.resourceId === "string" ? r.state.resourceId : r.id,
|
|
148
|
-
coalesceKey: typeof r.state.coalesceKey === "string" ? r.state.coalesceKey : null,
|
|
149
|
-
item: { ...r.state.remark, blockStateRef: { id: r.id, stateVersion: r.state_version } }
|
|
150
|
-
}))
|
|
151
|
-
);
|
|
152
|
-
if (stored.length > 0) dispatch(restageRemarks(conversationId, stored));
|
|
153
192
|
} catch (error) {
|
|
154
193
|
console.error("[remarks] restoring unsent chips failed:", error);
|
|
194
|
+
return;
|
|
155
195
|
} finally {
|
|
156
196
|
restoring.delete(conversationId);
|
|
157
197
|
}
|
|
198
|
+
restored.add(conversationId);
|
|
199
|
+
applyRows(conversationId, rows);
|
|
200
|
+
watch(conversationId);
|
|
158
201
|
})();
|
|
159
202
|
}
|
|
160
203
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remark-feed — the live feed of one conversation's block-state rows (PRIVATE Database Broadcast
|
|
3
|
+
* topics the block-state door sends on; event `block_state`, payload {op, row}):
|
|
4
|
+
* shared rows block_states:<scope>:<entity id>
|
|
5
|
+
* private rows block_states:<scope>:<entity id>:<user id>
|
|
6
|
+
* `scope` is `conversation` for chat answers. Refcounted: one pair of channels per unit.
|
|
7
|
+
* Namespace specs are identical to the host's own block feed, so both may declare them.
|
|
8
|
+
*/
|
|
9
|
+
export interface RemarkFeedRow {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
state_version: number;
|
|
13
|
+
sent_version: number;
|
|
14
|
+
state: Record<string, unknown>;
|
|
15
|
+
metadata: Record<string, unknown> | null;
|
|
16
|
+
}
|
|
17
|
+
export type RemarkFeedSignal = {
|
|
18
|
+
rows: RemarkFeedRow[];
|
|
19
|
+
} | {
|
|
20
|
+
resync: true;
|
|
21
|
+
};
|
|
22
|
+
export type RemarkFeedSubscribe = (unit: {
|
|
23
|
+
scope: string;
|
|
24
|
+
entityId: string;
|
|
25
|
+
}, userId: string, listener: (signal: RemarkFeedSignal) => void) => () => void;
|
|
26
|
+
export declare const subscribeRemarkFeed: RemarkFeedSubscribe;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineChannelNamespace, onRealtimeManagerChange } from "@ai-matrx/realtime";
|
|
2
|
+
const sharedChannel = defineChannelNamespace({
|
|
3
|
+
namespace: "block-states-shared",
|
|
4
|
+
parts: ["scope", "entityId"],
|
|
5
|
+
description: "Block state rows everyone on a record sees (private Database Broadcast)",
|
|
6
|
+
foreignTopic: "block_states"
|
|
7
|
+
});
|
|
8
|
+
const privateChannel = defineChannelNamespace({
|
|
9
|
+
namespace: "block-states-private",
|
|
10
|
+
parts: ["scope", "entityId", "viewerId"],
|
|
11
|
+
description: "One person's own block state rows on a record (private Database Broadcast)",
|
|
12
|
+
foreignTopic: "block_states"
|
|
13
|
+
});
|
|
14
|
+
const entries = /* @__PURE__ */ new Map();
|
|
15
|
+
function rowOf(value) {
|
|
16
|
+
if (!value || typeof value !== "object") return null;
|
|
17
|
+
const r = value;
|
|
18
|
+
if (typeof r.id !== "string" || typeof r.state_version !== "number") return null;
|
|
19
|
+
return {
|
|
20
|
+
id: r.id,
|
|
21
|
+
kind: String(r.kind ?? ""),
|
|
22
|
+
state_version: r.state_version,
|
|
23
|
+
sent_version: typeof r.sent_version === "number" ? r.sent_version : 0,
|
|
24
|
+
state: r.state && typeof r.state === "object" ? r.state : {},
|
|
25
|
+
metadata: r.metadata && typeof r.metadata === "object" ? r.metadata : null
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const subscribeRemarkFeed = (unit, userId, listener) => {
|
|
29
|
+
const id = `${unit.scope}:${unit.entityId}:${userId}`;
|
|
30
|
+
let entry = entries.get(id);
|
|
31
|
+
if (!entry) {
|
|
32
|
+
const created = { listeners: /* @__PURE__ */ new Set(), handles: [], stop: () => {
|
|
33
|
+
} };
|
|
34
|
+
const fan = (signal) => {
|
|
35
|
+
for (const l of Array.from(created.listeners)) l(signal);
|
|
36
|
+
};
|
|
37
|
+
const closeAll = () => {
|
|
38
|
+
for (const h of created.handles) h.close();
|
|
39
|
+
created.handles = [];
|
|
40
|
+
};
|
|
41
|
+
created.stop = onRealtimeManagerChange((manager) => {
|
|
42
|
+
closeAll();
|
|
43
|
+
if (!manager) return;
|
|
44
|
+
const open = (topic) => manager.open({
|
|
45
|
+
topic,
|
|
46
|
+
private: true,
|
|
47
|
+
wire: { mode: "raw", acceptEchoFromSelf: true },
|
|
48
|
+
broadcast: [
|
|
49
|
+
{
|
|
50
|
+
event: "block_state",
|
|
51
|
+
onMessage: ({ data }) => {
|
|
52
|
+
const row = rowOf(data?.row);
|
|
53
|
+
if (row) fan({ rows: [row] });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
eventKey: (_source, payload) => {
|
|
58
|
+
const frame = payload;
|
|
59
|
+
const row = frame?.row;
|
|
60
|
+
return row?.id === void 0 ? void 0 : `${String(row.id)}:${String(row.version)}:${String(row.state_version)}:${String(row.sent_version)}:${frame?.op ?? ""}`;
|
|
61
|
+
},
|
|
62
|
+
// Realtime has no replay: whatever happened while the socket was away is re-read.
|
|
63
|
+
onBackfill: () => fan({ resync: true })
|
|
64
|
+
});
|
|
65
|
+
created.handles = [
|
|
66
|
+
open(sharedChannel.topic({ scope: unit.scope, entityId: unit.entityId })),
|
|
67
|
+
open(privateChannel.topic({ scope: unit.scope, entityId: unit.entityId, viewerId: userId }))
|
|
68
|
+
];
|
|
69
|
+
});
|
|
70
|
+
entries.set(id, created);
|
|
71
|
+
entry = created;
|
|
72
|
+
}
|
|
73
|
+
entry.listeners.add(listener);
|
|
74
|
+
return () => {
|
|
75
|
+
const current = entries.get(id);
|
|
76
|
+
if (!current) return;
|
|
77
|
+
current.listeners.delete(listener);
|
|
78
|
+
if (current.listeners.size === 0) {
|
|
79
|
+
current.stop();
|
|
80
|
+
for (const h of current.handles) h.close();
|
|
81
|
+
entries.delete(id);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
export {
|
|
86
|
+
subscribeRemarkFeed
|
|
87
|
+
};
|
|
@@ -157,6 +157,16 @@ export interface RemarkDurability {
|
|
|
157
157
|
/** Write everything queued NOW and wait until every chip holds its saved row/ref. */
|
|
158
158
|
flush(conversationId: string): Promise<void>;
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Other writers whose saved state a send must wait for (a mounted interactive block's queued
|
|
162
|
+
* answers). They join the chips' flush: one call writes everything for the conversation.
|
|
163
|
+
*/
|
|
164
|
+
export interface PendingWritesPort {
|
|
165
|
+
hasPending(conversationId: string): boolean;
|
|
166
|
+
flush(conversationId: string): Promise<void>;
|
|
167
|
+
}
|
|
168
|
+
/** Join the send's flush. Returns the release. */
|
|
169
|
+
export declare function registerPendingWritesPort(port: PendingWritesPort): () => void;
|
|
160
170
|
/** True while some chip of this conversation has not yet been saved server-side. */
|
|
161
171
|
export declare function hasPendingRemarkWrites(conversationId: string): boolean;
|
|
162
172
|
/**
|
|
@@ -69,8 +69,15 @@ function buildSource(item, coalesceKey) {
|
|
|
69
69
|
text: remarkReadableText(item)
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
const pendingWritePorts = /* @__PURE__ */ new Set();
|
|
73
|
+
function registerPendingWritesPort(port) {
|
|
74
|
+
pendingWritePorts.add(port);
|
|
75
|
+
return () => {
|
|
76
|
+
pendingWritePorts.delete(port);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
72
79
|
function hasPendingRemarkWrites(conversationId) {
|
|
73
|
-
return durability?.hasPending(conversationId) ?? false;
|
|
80
|
+
return (durability?.hasPending(conversationId) ?? false) || [...pendingWritePorts].some((p) => p.hasPending(conversationId));
|
|
74
81
|
}
|
|
75
82
|
function isRemarkDurabilityRegistered() {
|
|
76
83
|
return durability !== null;
|
|
@@ -80,12 +87,12 @@ function announceMissingDurability(op) {
|
|
|
80
87
|
if (announcedMissing.has(op)) return;
|
|
81
88
|
announcedMissing.add(op);
|
|
82
89
|
console.error(
|
|
83
|
-
`[remarks] ${op} ran with no durability port registered: unsent chips are NOT kept server-side.
|
|
90
|
+
`[remarks] ${op} ran with no durability port registered: unsent chips are NOT kept server-side. <ChatProvider> registers the default; a host that replaced it must keep one registered.`
|
|
84
91
|
);
|
|
85
92
|
}
|
|
86
93
|
async function flushRemarkWrites(conversationId) {
|
|
87
94
|
if (!durability) announceMissingDurability("flush");
|
|
88
|
-
await durability?.flush(conversationId);
|
|
95
|
+
await Promise.all([durability?.flush(conversationId), ...[...pendingWritePorts].map((p) => p.flush(conversationId))]);
|
|
89
96
|
}
|
|
90
97
|
function restoreComposerRemarks(conversationId) {
|
|
91
98
|
if (!durability) announceMissingDurability("restore");
|
|
@@ -222,6 +229,7 @@ export {
|
|
|
222
229
|
hasPendingRemarkWrites,
|
|
223
230
|
isRemarkDurabilityRegistered,
|
|
224
231
|
readStoredRemarks,
|
|
232
|
+
registerPendingWritesPort,
|
|
225
233
|
registerRemarkDurability,
|
|
226
234
|
remarkChipTitle,
|
|
227
235
|
remarkKindLabel,
|