@adecore/agents 0.39.0 → 0.40.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/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex-models.json +3 -2
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/prices-snapshot.json +8 -0
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-models.json +3 -2
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/prices-snapshot.json +8 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/host/wiring.ts
CHANGED
|
@@ -67,6 +67,7 @@ async function installRulesIn(kind: AgentKind, folder: string, rules: CodexRules
|
|
|
67
67
|
if (kind !== 'codex') {
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
+
|
|
70
71
|
const path = codexRulesPathIn(folder, rules);
|
|
71
72
|
await installCodexRules(path, rules).catch((e: unknown) => console.error(`Could not write the codex rules in ${path}:`, errorText(e)));
|
|
72
73
|
}
|
|
@@ -82,6 +83,7 @@ export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWirin
|
|
|
82
83
|
const client = options.client ?? DEFAULT_CODEX_CLIENT;
|
|
83
84
|
const codexRules = options.codexRules;
|
|
84
85
|
const providers = new ProviderRegistry({ providers: [createClaudeProvider(options.claude), createCodexProvider({ client })], env, detect: options.detect });
|
|
86
|
+
|
|
85
87
|
const accounts = new ProviderAccountsService({
|
|
86
88
|
home: options.dataDir,
|
|
87
89
|
providers,
|
|
@@ -90,15 +92,19 @@ export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWirin
|
|
|
90
92
|
...(options.accountsHost ? { host: options.accountsHost } : {}),
|
|
91
93
|
...(codexRules ? { install: (kind: AgentKind, folder: string) => installRulesIn(kind, folder, codexRules) } : {})
|
|
92
94
|
});
|
|
95
|
+
|
|
93
96
|
if (codexRules) {
|
|
94
97
|
void installRulesIn('codex', defaultCodexHome(env), codexRules);
|
|
95
98
|
}
|
|
99
|
+
|
|
96
100
|
const nameOf = (kind: AgentKind | UsageProvider): string => providers.get(kind).name;
|
|
101
|
+
|
|
97
102
|
const limits = new UsageMonitor({ providers, accounts: limitAccountsOf(accounts, nameOf, env), client });
|
|
98
103
|
accounts.listen(() => limits.accountsChanged());
|
|
99
104
|
const attachments = new AttachmentStore(options.dataDir);
|
|
100
105
|
// Only without `core`, where `Core` is `ChatCore` itself.
|
|
101
106
|
const core = options.core ?? ((coreOptions: ChatCoreOptions) => new ChatCore(coreOptions) as Core);
|
|
107
|
+
|
|
102
108
|
const chats = core({
|
|
103
109
|
providers,
|
|
104
110
|
store: new ChatStore(options.dataDir, { attachments }),
|
|
@@ -116,6 +122,7 @@ export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWirin
|
|
|
116
122
|
claudeTitles: new ClaudeTitleReader(),
|
|
117
123
|
nameChat: (kind, input) => suggestChatTitle(providers, kind, input, definedEnv(env))
|
|
118
124
|
});
|
|
125
|
+
|
|
119
126
|
const usage = new UsageService({
|
|
120
127
|
home: options.dataDir,
|
|
121
128
|
knownProjects: () => Promise.resolve([]),
|
|
@@ -123,6 +130,7 @@ export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWirin
|
|
|
123
130
|
sessionAccounts: () => chatSessionAccounts(chats.list()),
|
|
124
131
|
accounts: () => usageAccountsOf(accounts, nameOf)
|
|
125
132
|
});
|
|
133
|
+
|
|
126
134
|
return {
|
|
127
135
|
providers,
|
|
128
136
|
accounts,
|
|
@@ -141,10 +149,12 @@ export function wireAgents<Core extends ChatCore = ChatCore>(options: AgentWirin
|
|
|
141
149
|
usage.subscribe(clientId, send),
|
|
142
150
|
limits.subscribe(clientId, send)
|
|
143
151
|
];
|
|
152
|
+
|
|
144
153
|
return () => {
|
|
145
154
|
for (const release of releases) {
|
|
146
155
|
release();
|
|
147
156
|
}
|
|
157
|
+
|
|
148
158
|
chats.detachAll(clientId);
|
|
149
159
|
usage.unfollow(clientId);
|
|
150
160
|
};
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { RecordDirectory, type RecordDirectoryOptions } from './record-directory
|
|
|
7
7
|
export { runProcess, type ProcessResult, type RunProcessOptions } from './run-process.ts';
|
|
8
8
|
export { Serializer, KeyedSerializer } from './serializer.ts';
|
|
9
9
|
export { cleanTitle, readLines } from './title-file.ts';
|
|
10
|
+
|
|
10
11
|
export {
|
|
11
12
|
type DirectoryWatcher,
|
|
12
13
|
type WatchSeams,
|
package/src/lineage.ts
CHANGED
|
@@ -60,24 +60,30 @@ export class AgentLineageStore {
|
|
|
60
60
|
descendants(nodeId: string): string[] {
|
|
61
61
|
const found: string[] = [];
|
|
62
62
|
const seen = new Set([nodeId]);
|
|
63
|
+
const entries = this.opened.all();
|
|
64
|
+
|
|
63
65
|
for (let i = -1; i < found.length; i++) {
|
|
64
66
|
const parent = i === -1 ? nodeId : found[i]!;
|
|
65
|
-
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
66
69
|
if (entry.openedBy === parent && openedByAgent(entry) && entry.endedAt === undefined && !seen.has(entry.nodeId)) {
|
|
67
70
|
seen.add(entry.nodeId);
|
|
68
71
|
found.push(entry.nodeId);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
|
|
72
76
|
return found;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/* Moves a node's record to another project with its dates as they were; false when nothing was written down for it or a prune took it meanwhile. */
|
|
76
80
|
async rehome(nodeId: string, projectId: string): Promise<boolean> {
|
|
77
81
|
const entry = this.opened.get(nodeId);
|
|
82
|
+
|
|
78
83
|
if (entry === undefined) {
|
|
79
84
|
return false;
|
|
80
85
|
}
|
|
86
|
+
|
|
81
87
|
return this.opened.write({ ...entry, projectId });
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -93,8 +99,10 @@ export class AgentLineageStore {
|
|
|
93
99
|
/* On the wall clock, like `createdAt`: a chat holds the mark against the time its turn started. */
|
|
94
100
|
async markEnded(nodeIds: readonly string[]): Promise<void> {
|
|
95
101
|
const at = Date.now();
|
|
102
|
+
|
|
96
103
|
for (const nodeId of nodeIds) {
|
|
97
104
|
const entry = this.opened.get(nodeId);
|
|
105
|
+
|
|
98
106
|
if (entry && entry.endedAt === undefined) {
|
|
99
107
|
await this.opened.write({ ...entry, endedAt: at });
|
|
100
108
|
}
|
|
@@ -103,7 +111,8 @@ export class AgentLineageStore {
|
|
|
103
111
|
|
|
104
112
|
/* The nodes of this project whose opener it no longer places, and that no cascade ended yet. */
|
|
105
113
|
orphans(projectId: string, ids: ReadonlySet<string>): Array<{ nodeId: string; openedBy: string }> {
|
|
106
|
-
return
|
|
114
|
+
return this.opened
|
|
115
|
+
.all()
|
|
107
116
|
.filter(
|
|
108
117
|
(entry) =>
|
|
109
118
|
entry.projectId === projectId && openedByAgent(entry) && entry.endedAt === undefined && ids.has(entry.nodeId) && !ids.has(entry.openedBy)
|
|
@@ -114,6 +123,7 @@ export class AgentLineageStore {
|
|
|
114
123
|
/* The widest mode this agent node may start in, every time it starts; null for one no agent opened. */
|
|
115
124
|
ceilingOf(nodeId: string): RuntimeMode | null {
|
|
116
125
|
const entry = this.opened.get(nodeId);
|
|
126
|
+
|
|
117
127
|
return entry !== undefined && openedByAgent(entry) ? (entry.ceiling ?? null) : null;
|
|
118
128
|
}
|
|
119
129
|
|
|
@@ -122,7 +132,7 @@ export class AgentLineageStore {
|
|
|
122
132
|
return this.opened.get(nodeId)?.peer ?? null;
|
|
123
133
|
}
|
|
124
134
|
|
|
125
|
-
/*
|
|
135
|
+
/* A node nobody wrote down is one a person made, which is where a chain starts. */
|
|
126
136
|
depthOf(nodeId: string): number {
|
|
127
137
|
return this.opened.get(nodeId)?.depth ?? 0;
|
|
128
138
|
}
|
|
@@ -130,39 +140,48 @@ export class AgentLineageStore {
|
|
|
130
140
|
// A host may exclude completed hidden agents while keeping their lineage for later reads and tasks.
|
|
131
141
|
openedCount(callerId: string, include: (nodeId: string) => boolean = () => true): number {
|
|
132
142
|
let count = 0;
|
|
143
|
+
|
|
133
144
|
for (const entry of this.opened.all()) {
|
|
134
145
|
if (entry.openedBy === callerId && openedByAgent(entry) && include(entry.nodeId)) {
|
|
135
146
|
count += 1;
|
|
136
147
|
}
|
|
137
148
|
}
|
|
149
|
+
|
|
138
150
|
return count;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
/* Who made this node, or null for one a person made: the whole of the rule node delete follows. */
|
|
142
154
|
madeBy(nodeId: string): string | null {
|
|
143
155
|
const entry = this.opened.get(nodeId);
|
|
156
|
+
|
|
144
157
|
return entry === undefined || entry.relation === 'fork' ? null : entry.openedBy;
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
/* The node that opened this one as an agent, or null for a node no agent or team started. */
|
|
148
161
|
startedBy(nodeId: string): string | null {
|
|
149
162
|
const entry = this.opened.get(nodeId);
|
|
163
|
+
|
|
150
164
|
return entry !== undefined && openedByAgent(entry) ? entry.openedBy : null;
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
/* The chat a fork was made from, or null for a node that is no fork. */
|
|
154
168
|
forkedFrom(nodeId: string): string | null {
|
|
155
169
|
const entry = this.opened.get(nodeId);
|
|
170
|
+
|
|
156
171
|
return entry?.relation === 'fork' ? entry.openedBy : null;
|
|
157
172
|
}
|
|
158
173
|
|
|
159
174
|
forksOf(nodeId: string): string[] {
|
|
160
|
-
return
|
|
175
|
+
return this.opened
|
|
176
|
+
.all()
|
|
177
|
+
.filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId)
|
|
178
|
+
.map((entry) => entry.nodeId);
|
|
161
179
|
}
|
|
162
180
|
|
|
163
181
|
/* The forks of this project whose node is gone, read before `prune` forgets them. */
|
|
164
182
|
forksLeaving(projectId: string, ids: ReadonlySet<string>): string[] {
|
|
165
|
-
return
|
|
183
|
+
return this.opened
|
|
184
|
+
.all()
|
|
166
185
|
.filter((entry) => entry.projectId === projectId && entry.relation === 'fork' && !ids.has(entry.nodeId))
|
|
167
186
|
.map((entry) => entry.nodeId);
|
|
168
187
|
}
|
|
@@ -36,18 +36,24 @@ export function deliverMessageHandler(deps: DeliverMessageDeps) {
|
|
|
36
36
|
if (!deps.placed(entry.target)) {
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
+
|
|
39
40
|
const waiting = deps.notices.waiting(entry.target);
|
|
41
|
+
|
|
40
42
|
// A turn opened for an earlier message took these along, or they went stale: nothing to wake anyone for.
|
|
41
43
|
if (waiting.length === 0) {
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
const chat = await deps.chat(entry.target);
|
|
48
|
+
|
|
45
49
|
if (chat === null) {
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
52
|
+
|
|
48
53
|
if (chat.relocating?.() === true) {
|
|
49
54
|
return 'wait';
|
|
50
55
|
}
|
|
56
|
+
|
|
51
57
|
// No note and no preamble on the thread: a person read the message as it landed (`showNotices`),
|
|
52
58
|
// and the label on the turn names who it came from.
|
|
53
59
|
chat.wake({
|
|
@@ -40,18 +40,23 @@ export interface ChatDelivery {
|
|
|
40
40
|
*/
|
|
41
41
|
export function turnFromMessage(items: readonly ChatItem[], activeTurnId: string | null): boolean {
|
|
42
42
|
const from = activeTurnId === null ? items.findLastIndex((item) => item.kind === 'turn') : items.findIndex((item) => item.id === activeTurnId);
|
|
43
|
+
|
|
43
44
|
for (let i = from; i >= 0; i--) {
|
|
44
45
|
const turn = items[i]!;
|
|
46
|
+
|
|
45
47
|
if (turn.kind !== 'turn') {
|
|
46
48
|
continue;
|
|
47
49
|
}
|
|
50
|
+
|
|
48
51
|
if ((turn.messageFrom ?? []).length > 0) {
|
|
49
52
|
return true;
|
|
50
53
|
}
|
|
54
|
+
|
|
51
55
|
if (turn.origin !== 'agent' || turn.taskIds !== undefined || turn.summaryFor !== undefined) {
|
|
52
56
|
return false;
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
|
|
55
60
|
return false;
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -60,14 +65,17 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
|
|
|
60
65
|
return {
|
|
61
66
|
chat: async (id) => {
|
|
62
67
|
const session = chats.get(id);
|
|
68
|
+
|
|
63
69
|
if (session) {
|
|
64
70
|
return session.info.activeTurnId === null ? 'idle' : 'running';
|
|
65
71
|
}
|
|
72
|
+
|
|
66
73
|
// A chat nobody has loaded is idle: the turn opens on the thread the host reads back from disk.
|
|
67
74
|
return (await chats.hasStored(id)) ? 'idle' : 'none';
|
|
68
75
|
},
|
|
69
76
|
fromMessage: (id) => {
|
|
70
77
|
const session = chats.get(id);
|
|
78
|
+
|
|
71
79
|
return session !== undefined && turnFromMessage(session.thread.list(), session.info.activeTurnId);
|
|
72
80
|
}
|
|
73
81
|
};
|
|
@@ -82,15 +90,19 @@ export function chatNoticeTargets(chats: Pick<ChatCore, 'get' | 'hasStored'>): C
|
|
|
82
90
|
export async function deliverToChat(store: Pick<NoticeStore, 'put'>, targets: ChatNoticeTargets, notice: Omit<Notice, 'createdAt'>): Promise<ChatDelivery> {
|
|
83
91
|
const waiting = await store.put(notice);
|
|
84
92
|
const chat = await targets.chat(notice.targetId);
|
|
93
|
+
|
|
85
94
|
if (chat === 'none') {
|
|
86
95
|
return { outcome: 'no-chat', waiting };
|
|
87
96
|
}
|
|
97
|
+
|
|
88
98
|
if (chat === 'running') {
|
|
89
99
|
return { outcome: 'in-turn', waiting };
|
|
90
100
|
}
|
|
101
|
+
|
|
91
102
|
if (targets.fromMessage(notice.from)) {
|
|
92
103
|
return { outcome: 'from-message', waiting };
|
|
93
104
|
}
|
|
105
|
+
|
|
94
106
|
return { outcome: 'wake', waiting };
|
|
95
107
|
}
|
|
96
108
|
|
|
@@ -110,6 +122,7 @@ export async function showNotices(store: Pick<NoticeStore, 'show'>, chat: Notice
|
|
|
110
122
|
if (!(await chat.has(targetId))) {
|
|
111
123
|
return;
|
|
112
124
|
}
|
|
125
|
+
|
|
113
126
|
for (const notice of await store.show(targetId)) {
|
|
114
127
|
await chat.note(targetId, words.shown(notice));
|
|
115
128
|
}
|
|
@@ -37,10 +37,9 @@ const FileSchema = z.object({ notices: z.array(NoticeSchema).min(1) });
|
|
|
37
37
|
type NoticeFile = z.infer<typeof FileSchema>;
|
|
38
38
|
|
|
39
39
|
/*
|
|
40
|
-
* The messages waiting for each node, held until that node's agent has a moment to hear them
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* first.
|
|
40
|
+
* The messages waiting for each node, held until that node's agent has a moment to hear them. On disk
|
|
41
|
+
* because the receiver may well be a node nobody has mounted yet, and a host that restarts must not
|
|
42
|
+
* lose what was left for it.
|
|
44
43
|
*/
|
|
45
44
|
export class NoticeStore {
|
|
46
45
|
readonly dir: string;
|
|
@@ -51,6 +50,7 @@ export class NoticeStore {
|
|
|
51
50
|
constructor(dataDir: string, now: () => number = Date.now) {
|
|
52
51
|
this.dir = join(dataDir, 'notices');
|
|
53
52
|
this.now = now;
|
|
53
|
+
|
|
54
54
|
this.queues = new RecordDirectory({
|
|
55
55
|
dir: this.dir,
|
|
56
56
|
schema: FileSchema,
|
|
@@ -58,6 +58,7 @@ export class NoticeStore {
|
|
|
58
58
|
// What went stale while the host was down is not delivered; a queue of nothing else goes.
|
|
59
59
|
keep: (file) => {
|
|
60
60
|
const fresh = file.notices.filter((notice) => this.fresh(notice));
|
|
61
|
+
|
|
61
62
|
return fresh[0] === undefined ? null : { notices: fresh };
|
|
62
63
|
}
|
|
63
64
|
});
|
|
@@ -71,6 +72,7 @@ export class NoticeStore {
|
|
|
71
72
|
async put(notice: Omit<Notice, 'createdAt'>): Promise<number> {
|
|
72
73
|
const queue = [...this.waiting(notice.targetId), { ...notice, createdAt: this.now() }].slice(-MAX_NOTICES);
|
|
73
74
|
await this.persist(notice.targetId, queue);
|
|
75
|
+
|
|
74
76
|
return queue.length;
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -82,10 +84,13 @@ export class NoticeStore {
|
|
|
82
84
|
*/
|
|
83
85
|
take(targetId: string): Notice[] {
|
|
84
86
|
const queue = this.waiting(targetId);
|
|
87
|
+
|
|
85
88
|
if (queue.length === 0) {
|
|
86
89
|
return [];
|
|
87
90
|
}
|
|
91
|
+
|
|
88
92
|
this.lastDrop = this.persist(targetId, []).catch((e) => console.error(`Dropping the messages of ${targetId} failed:`, errorText(e)));
|
|
93
|
+
|
|
89
94
|
return queue;
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -101,22 +106,27 @@ export class NoticeStore {
|
|
|
101
106
|
async show(targetId: string): Promise<Notice[]> {
|
|
102
107
|
const queue = this.waiting(targetId);
|
|
103
108
|
const unshown = queue.filter((notice) => notice.shown !== true);
|
|
109
|
+
|
|
104
110
|
if (unshown.length === 0) {
|
|
105
111
|
return [];
|
|
106
112
|
}
|
|
113
|
+
|
|
107
114
|
await this.persist(
|
|
108
115
|
targetId,
|
|
109
116
|
queue.map((notice) => ({ ...notice, shown: true }))
|
|
110
117
|
);
|
|
118
|
+
|
|
111
119
|
return unshown;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
/* What waits for a node, without taking it; the stale ones are already gone from the answer. */
|
|
115
123
|
waiting(targetId: string): Notice[] {
|
|
116
124
|
const file = this.queues.get(targetId);
|
|
125
|
+
|
|
117
126
|
if (file === undefined) {
|
|
118
127
|
return [];
|
|
119
128
|
}
|
|
129
|
+
|
|
120
130
|
return file.notices.filter((notice) => this.fresh(notice));
|
|
121
131
|
}
|
|
122
132
|
|
|
@@ -133,8 +143,10 @@ export class NoticeStore {
|
|
|
133
143
|
private async persist(targetId: string, queue: Notice[]): Promise<void> {
|
|
134
144
|
if (queue.length === 0) {
|
|
135
145
|
await this.queues.remove(targetId);
|
|
146
|
+
|
|
136
147
|
return;
|
|
137
148
|
}
|
|
149
|
+
|
|
138
150
|
await this.queues.write({ notices: queue });
|
|
139
151
|
}
|
|
140
152
|
}
|
package/src/modes.ts
CHANGED
|
@@ -67,6 +67,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
67
67
|
|
|
68
68
|
stop(): void {
|
|
69
69
|
this.started = false;
|
|
70
|
+
|
|
70
71
|
if (this.timer !== null) {
|
|
71
72
|
this.clock.clearTimeout(this.timer);
|
|
72
73
|
this.timer = null;
|
|
@@ -87,11 +88,13 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
87
88
|
wake(target: string): void {
|
|
88
89
|
this.wakes.set(target, (this.wakes.get(target) ?? 0) + 1);
|
|
89
90
|
let woke = false;
|
|
91
|
+
|
|
90
92
|
for (const entry of this.store.list()) {
|
|
91
93
|
if (entry.target === target && this.waiting.delete(entry.id)) {
|
|
92
94
|
woke = true;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
97
|
+
|
|
95
98
|
if (woke) {
|
|
96
99
|
queueMicrotask(() => this.drain());
|
|
97
100
|
}
|
|
@@ -102,6 +105,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
102
105
|
if (this.isSettled()) {
|
|
103
106
|
return Promise.resolve();
|
|
104
107
|
}
|
|
108
|
+
|
|
105
109
|
return new Promise((resolve) => {
|
|
106
110
|
this.waiters.push(resolve);
|
|
107
111
|
});
|
|
@@ -110,57 +114,76 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
110
114
|
private drain(): void {
|
|
111
115
|
if (!this.started) {
|
|
112
116
|
this.notifySettled();
|
|
117
|
+
|
|
113
118
|
return;
|
|
114
119
|
}
|
|
120
|
+
|
|
115
121
|
const now = this.clock.now();
|
|
116
122
|
const busy = new Set(this.running);
|
|
117
123
|
let nextDue: number | null = null;
|
|
124
|
+
|
|
118
125
|
const dueAt = (at: number): void => {
|
|
119
126
|
nextDue = nextDue === null ? at : Math.min(nextDue, at);
|
|
120
127
|
};
|
|
128
|
+
|
|
121
129
|
for (const entry of this.store.list()) {
|
|
122
130
|
// A waiting entry holds no lane. The resume of a chat must not queue behind a wake that waits for it.
|
|
123
131
|
if (busy.has(entry.target) || this.waiting.has(entry.id)) {
|
|
124
132
|
continue;
|
|
125
133
|
}
|
|
134
|
+
|
|
126
135
|
// Nor does work due at a later time, which nothing owed before it has to wait for.
|
|
127
136
|
if (entry.attempts === 0 && entry.notBefore > now) {
|
|
128
137
|
dueAt(entry.notBefore);
|
|
138
|
+
|
|
129
139
|
continue;
|
|
130
140
|
}
|
|
141
|
+
|
|
131
142
|
const lanes = this.store.lanesOf(entry);
|
|
143
|
+
|
|
132
144
|
// Oldest first per target. A younger entry never overtakes one that waits out a retry.
|
|
133
145
|
for (const lane of lanes) {
|
|
134
146
|
busy.add(lane);
|
|
135
147
|
}
|
|
148
|
+
|
|
136
149
|
// The other lanes only wait for work that is running; work that is merely owed there is what ending takes away.
|
|
137
150
|
if (lanes.some((lane) => lane !== entry.target && this.running.has(lane))) {
|
|
138
151
|
continue;
|
|
139
152
|
}
|
|
153
|
+
|
|
140
154
|
if (entry.notBefore > now) {
|
|
141
155
|
dueAt(entry.notBefore);
|
|
156
|
+
|
|
142
157
|
continue;
|
|
143
158
|
}
|
|
159
|
+
|
|
144
160
|
void this.run(entry);
|
|
145
161
|
}
|
|
162
|
+
|
|
146
163
|
this.schedule(nextDue, now);
|
|
147
164
|
this.notifySettled();
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
private async run(entry: OutboxEntryOf<Work>): Promise<void> {
|
|
151
168
|
const lanes = this.store.lanesOf(entry);
|
|
169
|
+
|
|
152
170
|
for (const lane of lanes) {
|
|
153
171
|
this.running.add(lane);
|
|
154
172
|
}
|
|
173
|
+
|
|
155
174
|
const woken = this.wakes.get(entry.target) ?? 0;
|
|
175
|
+
|
|
156
176
|
try {
|
|
157
177
|
const outcome = await (this.handlers[entry.kind as Work['kind']] as (entry: OutboxEntryOf<Work>) => Promise<OutboxOutcome>)(entry);
|
|
178
|
+
|
|
158
179
|
if (outcome === 'wait') {
|
|
159
180
|
if ((this.wakes.get(entry.target) ?? 0) === woken && this.store.has(entry.id)) {
|
|
160
181
|
this.waiting.add(entry.id);
|
|
161
182
|
}
|
|
183
|
+
|
|
162
184
|
return;
|
|
163
185
|
}
|
|
186
|
+
|
|
164
187
|
await this.store.remove(entry.id);
|
|
165
188
|
} catch (e) {
|
|
166
189
|
await this.failed(entry, e).catch((error: unknown) => console.error(`The outbox could not record a failed ${entry.kind}:`, errorText(error)));
|
|
@@ -168,18 +191,22 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
168
191
|
for (const lane of lanes) {
|
|
169
192
|
this.running.delete(lane);
|
|
170
193
|
}
|
|
194
|
+
|
|
171
195
|
this.drain();
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
private async failed(entry: OutboxEntryOf<Work>, error: unknown): Promise<void> {
|
|
176
200
|
const delay = RETRY_DELAYS_MS[entry.attempts];
|
|
201
|
+
|
|
177
202
|
if (delay === undefined) {
|
|
178
203
|
await this.store.remove(entry.id);
|
|
179
204
|
console.error(`Gave up on ${entry.kind} for ${entry.target}:`, errorText(error));
|
|
180
205
|
this.onParked(entry, error);
|
|
206
|
+
|
|
181
207
|
return;
|
|
182
208
|
}
|
|
209
|
+
|
|
183
210
|
await this.store.update({ ...entry, attempts: entry.attempts + 1, notBefore: this.clock.now() + delay });
|
|
184
211
|
}
|
|
185
212
|
|
|
@@ -188,6 +215,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
188
215
|
this.clock.clearTimeout(this.timer);
|
|
189
216
|
this.timer = null;
|
|
190
217
|
}
|
|
218
|
+
|
|
191
219
|
if (nextDue !== null) {
|
|
192
220
|
this.timer = this.clock.setTimeout(
|
|
193
221
|
() => {
|
|
@@ -201,6 +229,7 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
201
229
|
|
|
202
230
|
private isSettled(): boolean {
|
|
203
231
|
const now = this.clock.now();
|
|
232
|
+
|
|
204
233
|
return this.running.size === 0 && (!this.started || !this.store.list().some((entry) => entry.notBefore <= now && !this.waiting.has(entry.id)));
|
|
205
234
|
}
|
|
206
235
|
|
|
@@ -208,8 +237,10 @@ export class OutboxWorker<Work extends OutboxWorkShape> {
|
|
|
208
237
|
if (!this.isSettled()) {
|
|
209
238
|
return;
|
|
210
239
|
}
|
|
240
|
+
|
|
211
241
|
const waiting = this.waiters;
|
|
212
242
|
this.waiters = [];
|
|
243
|
+
|
|
213
244
|
for (const resolve of waiting) {
|
|
214
245
|
resolve();
|
|
215
246
|
}
|
package/src/outbox/outbox.ts
CHANGED
|
@@ -45,11 +45,13 @@ export class OutboxStore<Work extends OutboxWorkShape> {
|
|
|
45
45
|
|
|
46
46
|
constructor(options: OutboxStoreOptions<Work>) {
|
|
47
47
|
this.dir = join(options.dataDir, 'outbox');
|
|
48
|
+
|
|
48
49
|
this.entries = new RecordDirectory<OutboxEntryOf<Work>>({
|
|
49
50
|
dir: this.dir,
|
|
50
51
|
schema: z.intersection(options.work, OutboxEntryFieldsSchema),
|
|
51
52
|
idOf: (entry) => entry.id
|
|
52
53
|
});
|
|
54
|
+
|
|
53
55
|
this.lanes = options.lanesOf ?? ((entry) => [entry.target]);
|
|
54
56
|
this.outlivesTarget = options.outlivesTarget ?? (() => false);
|
|
55
57
|
}
|
|
@@ -70,7 +72,9 @@ export class OutboxStore<Work extends OutboxWorkShape> {
|
|
|
70
72
|
attempts: 0,
|
|
71
73
|
notBefore
|
|
72
74
|
};
|
|
75
|
+
|
|
73
76
|
await this.entries.write(entry);
|
|
77
|
+
|
|
74
78
|
return entry;
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -79,6 +83,7 @@ export class OutboxStore<Work extends OutboxWorkShape> {
|
|
|
79
83
|
if (!this.entries.has(entry.id)) {
|
|
80
84
|
return;
|
|
81
85
|
}
|
|
86
|
+
|
|
82
87
|
await this.entries.write(entry);
|
|
83
88
|
}
|
|
84
89
|
|