@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/chat/codex-thread.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface ThreadItemsParams {
|
|
|
18
18
|
export function parseThreadItemsPage(result: unknown): { entries: Record<string, unknown>[]; nextCursor: string | null } {
|
|
19
19
|
const data = isRecord(result) && Array.isArray(result.data) ? result.data.filter(isRecord) : [];
|
|
20
20
|
const nextCursor = isRecord(result) && typeof result.nextCursor === 'string' && result.nextCursor !== '' ? result.nextCursor : null;
|
|
21
|
+
|
|
21
22
|
return { entries: data, nextCursor };
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -40,23 +41,31 @@ function userText(content: unknown): string {
|
|
|
40
41
|
export function projectCodexItems(entriesOldestFirst: Record<string, unknown>[], now: number): ChatItem[] {
|
|
41
42
|
const { thread, projector } = readingThread('Codex', () => now);
|
|
42
43
|
const protocol = new CodexProtocol(0);
|
|
44
|
+
|
|
43
45
|
for (const entry of entriesOldestFirst) {
|
|
44
46
|
const item = isRecord(entry.item) ? entry.item : null;
|
|
47
|
+
|
|
45
48
|
if (!item || typeof item.id !== 'string') {
|
|
46
49
|
continue;
|
|
47
50
|
}
|
|
51
|
+
|
|
48
52
|
projector.reset();
|
|
53
|
+
|
|
49
54
|
if (item.type === 'userMessage') {
|
|
50
55
|
const text = userText(item.content);
|
|
56
|
+
|
|
51
57
|
if (text !== '') {
|
|
52
58
|
thread.upsert({ id: `user-${item.id}`, kind: 'user', createdAt: now, turnId: null, text });
|
|
53
59
|
}
|
|
60
|
+
|
|
54
61
|
continue;
|
|
55
62
|
}
|
|
63
|
+
|
|
56
64
|
for (const event of protocol.handle({ method: 'item/completed', params: { item } })) {
|
|
57
65
|
projector.project(0, event);
|
|
58
66
|
}
|
|
59
67
|
}
|
|
68
|
+
|
|
60
69
|
return thread.list().map(settledReading);
|
|
61
70
|
}
|
|
62
71
|
|
|
@@ -78,12 +87,15 @@ async function withAppServer<T>(spec: CodexProcessSpec, work: (transport: CodexT
|
|
|
78
87
|
onFrame: () => undefined,
|
|
79
88
|
onExit: () => undefined
|
|
80
89
|
});
|
|
90
|
+
|
|
81
91
|
try {
|
|
82
92
|
await transport.request('initialize', {
|
|
83
93
|
clientInfo: spec.client ?? DEFAULT_CODEX_CLIENT,
|
|
84
94
|
capabilities: { experimentalApi: true, requestAttestation: false }
|
|
85
95
|
});
|
|
96
|
+
|
|
86
97
|
transport.notify('initialized', {});
|
|
98
|
+
|
|
87
99
|
return await work(transport);
|
|
88
100
|
} finally {
|
|
89
101
|
transport.end();
|
|
@@ -112,17 +124,21 @@ export interface ThreadForkParams {
|
|
|
112
124
|
async function nthTurnId(transport: CodexTransport, threadId: string, turns: number): Promise<string | null> {
|
|
113
125
|
let seen = 0;
|
|
114
126
|
let cursor: string | null = null;
|
|
127
|
+
|
|
115
128
|
do {
|
|
116
129
|
const page = parseThreadItemsPage(
|
|
117
130
|
await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
|
|
118
131
|
);
|
|
132
|
+
|
|
119
133
|
for (const turn of page.entries) {
|
|
120
134
|
if (++seen === turns) {
|
|
121
135
|
return typeof turn.id === 'string' ? turn.id : null;
|
|
122
136
|
}
|
|
123
137
|
}
|
|
138
|
+
|
|
124
139
|
cursor = page.nextCursor;
|
|
125
140
|
} while (cursor !== null);
|
|
141
|
+
|
|
126
142
|
return null;
|
|
127
143
|
}
|
|
128
144
|
|
|
@@ -134,13 +150,17 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
|
|
|
134
150
|
export function forkThreadOnce(spec: CodexProcessSpec, params: ThreadForkParams): Promise<string> {
|
|
135
151
|
return withAppServer(spec, async (transport) => {
|
|
136
152
|
let lastTurnId: string | null = null;
|
|
153
|
+
|
|
137
154
|
if (params.at !== null) {
|
|
138
155
|
lastTurnId = 'turnId' in params.at ? params.at.turnId : await nthTurnId(transport, params.threadId, params.at.turns);
|
|
156
|
+
|
|
139
157
|
if (lastTurnId === null) {
|
|
140
158
|
throw new ChatError('turn-not-found', 'Codex keeps fewer turns in this thread than the chat shows, so the turn could not be found');
|
|
141
159
|
}
|
|
142
160
|
}
|
|
161
|
+
|
|
143
162
|
let result: unknown;
|
|
163
|
+
|
|
144
164
|
try {
|
|
145
165
|
result = await transport.request('thread/fork', {
|
|
146
166
|
threadId: params.threadId,
|
|
@@ -151,10 +171,13 @@ export function forkThreadOnce(spec: CodexProcessSpec, params: ThreadForkParams)
|
|
|
151
171
|
} catch (error) {
|
|
152
172
|
throw new ChatError('fork-failed', errorText(error));
|
|
153
173
|
}
|
|
174
|
+
|
|
154
175
|
const thread = isRecord(result) && isRecord(result.thread) ? result.thread : null;
|
|
176
|
+
|
|
155
177
|
if (thread === null || typeof thread.id !== 'string' || thread.id === '') {
|
|
156
178
|
throw new ChatError('fork-failed', 'Codex answered the fork without a thread');
|
|
157
179
|
}
|
|
180
|
+
|
|
158
181
|
return thread.id;
|
|
159
182
|
});
|
|
160
183
|
}
|
|
@@ -39,22 +39,27 @@ export class ComposerPreferences {
|
|
|
39
39
|
/* The newest pick among the connected clients, narrowed to this provider; empty with none. */
|
|
40
40
|
for(provider: AgentKind): ComposerPreference {
|
|
41
41
|
const newest = this.newest();
|
|
42
|
+
|
|
42
43
|
if (newest === null) {
|
|
43
44
|
return {};
|
|
44
45
|
}
|
|
46
|
+
|
|
45
47
|
const { runtimeMode, selections, accounts } = newest.preference;
|
|
46
48
|
const selection = selections?.[provider];
|
|
47
49
|
const account = accounts?.[provider];
|
|
50
|
+
|
|
48
51
|
return { ...(runtimeMode ? { runtimeMode } : {}), ...(selection ? { selection } : {}), ...(account ? { account } : {}) };
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
private newest(): Held | null {
|
|
52
55
|
let newest: Held | null = null;
|
|
56
|
+
|
|
53
57
|
for (const held of this.byClient.values()) {
|
|
54
58
|
if (newest === null || isNewer(held, newest)) {
|
|
55
59
|
newest = held;
|
|
56
60
|
}
|
|
57
61
|
}
|
|
62
|
+
|
|
58
63
|
return newest;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
@@ -62,5 +67,6 @@ export class ComposerPreferences {
|
|
|
62
67
|
function isNewer(held: Held, than: Held): boolean {
|
|
63
68
|
const changedAt = held.preference.changedAt ?? 0;
|
|
64
69
|
const otherChangedAt = than.preference.changedAt ?? 0;
|
|
70
|
+
|
|
65
71
|
return changedAt === otherChangedAt ? held.told > than.told : changedAt > otherChangedAt;
|
|
66
72
|
}
|
|
@@ -21,6 +21,7 @@ function inputTokens(input: unknown): number {
|
|
|
21
21
|
if (input === undefined || input === null) {
|
|
22
22
|
return 0;
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
return textTokens(typeof input === 'string' ? input : JSON.stringify(input));
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -35,27 +36,33 @@ export function estimateContextBreakdown(items: readonly ChatItem[], contextToke
|
|
|
35
36
|
let filesRead = 0;
|
|
36
37
|
let conversation = 0;
|
|
37
38
|
const start = items.findLastIndex((item) => item.kind === 'compaction') + 1;
|
|
39
|
+
|
|
38
40
|
for (const item of items.slice(start)) {
|
|
39
41
|
switch (item.kind) {
|
|
40
42
|
case 'user':
|
|
41
43
|
conversation += textTokens(item.text);
|
|
42
44
|
filesRead += (item.attachments ?? []).filter((attachment) => attachmentImageMime(attachment) !== null).length * IMAGE_TOKENS;
|
|
45
|
+
|
|
43
46
|
break;
|
|
44
47
|
case 'assistant':
|
|
45
48
|
if (!item.parentToolUseId) {
|
|
46
49
|
conversation += textTokens(item.text);
|
|
47
50
|
}
|
|
51
|
+
|
|
48
52
|
break;
|
|
49
53
|
case 'tool': {
|
|
50
54
|
if (item.parentToolUseId !== null) {
|
|
51
55
|
break;
|
|
52
56
|
}
|
|
57
|
+
|
|
53
58
|
const tokens = inputTokens(item.input) + textTokens(item.output ?? item.progress?.output ?? '');
|
|
59
|
+
|
|
54
60
|
if (isReadTool(item.name)) {
|
|
55
61
|
filesRead += tokens;
|
|
56
62
|
} else {
|
|
57
63
|
toolOutput += tokens;
|
|
58
64
|
}
|
|
65
|
+
|
|
59
66
|
break;
|
|
60
67
|
}
|
|
61
68
|
case 'subagent':
|
|
@@ -63,18 +70,22 @@ export function estimateContextBreakdown(items: readonly ChatItem[], contextToke
|
|
|
63
70
|
if (item.origin !== HOST_TASK_ORIGIN) {
|
|
64
71
|
toolOutput += textTokens(item.prompt ?? '') + textTokens(item.result ?? '');
|
|
65
72
|
}
|
|
73
|
+
|
|
66
74
|
break;
|
|
67
75
|
default:
|
|
68
76
|
break;
|
|
69
77
|
}
|
|
70
78
|
}
|
|
79
|
+
|
|
71
80
|
const total = Math.max(0, Math.round(contextTokens));
|
|
72
81
|
const estimated = toolOutput + filesRead + conversation;
|
|
73
82
|
const scale = estimated > total ? total / estimated : 1;
|
|
83
|
+
|
|
74
84
|
const parts = {
|
|
75
85
|
toolOutput: Math.floor(toolOutput * scale),
|
|
76
86
|
filesRead: Math.floor(filesRead * scale),
|
|
77
87
|
conversation: Math.floor(conversation * scale)
|
|
78
88
|
};
|
|
89
|
+
|
|
79
90
|
return { ...parts, system: Math.max(0, total - parts.toolOutput - parts.filesRead - parts.conversation) };
|
|
80
91
|
}
|
|
@@ -24,12 +24,16 @@ export class DeltaCoalescer {
|
|
|
24
24
|
if (event.type !== 'delta' || event.uiQueries !== undefined) {
|
|
25
25
|
this.flush();
|
|
26
26
|
this.send(event);
|
|
27
|
+
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
if (this.pending !== null && this.pending.itemId === event.itemId) {
|
|
30
32
|
this.pending.text += event.text;
|
|
33
|
+
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
36
|
+
|
|
33
37
|
this.flush();
|
|
34
38
|
this.pending = { itemId: event.itemId, text: event.text };
|
|
35
39
|
this.timer = setTimeout(() => this.flush(), this.tickMs);
|
|
@@ -43,10 +47,13 @@ export class DeltaCoalescer {
|
|
|
43
47
|
clearTimeout(this.timer);
|
|
44
48
|
this.timer = null;
|
|
45
49
|
}
|
|
50
|
+
|
|
46
51
|
const pending = this.pending;
|
|
52
|
+
|
|
47
53
|
if (pending === null) {
|
|
48
54
|
return;
|
|
49
55
|
}
|
|
56
|
+
|
|
50
57
|
this.pending = null;
|
|
51
58
|
this.send({ type: 'delta', itemId: pending.itemId, text: pending.text });
|
|
52
59
|
}
|
|
@@ -57,6 +64,7 @@ export class DeltaCoalescer {
|
|
|
57
64
|
clearTimeout(this.timer);
|
|
58
65
|
this.timer = null;
|
|
59
66
|
}
|
|
67
|
+
|
|
60
68
|
this.pending = null;
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -33,13 +33,16 @@ function titleOf(text: string): string {
|
|
|
33
33
|
|
|
34
34
|
function records(head: string): Array<Record<string, unknown>> {
|
|
35
35
|
const lines = head.split('\n');
|
|
36
|
+
|
|
36
37
|
// The head ends wherever the byte limit fell, so its last line may not parse.
|
|
37
38
|
return lines.flatMap((line) => {
|
|
38
39
|
if (line === '') {
|
|
39
40
|
return [];
|
|
40
41
|
}
|
|
42
|
+
|
|
41
43
|
try {
|
|
42
44
|
const parsed: unknown = JSON.parse(line);
|
|
45
|
+
|
|
43
46
|
return isRecord(parsed) ? [parsed] : [];
|
|
44
47
|
} catch {
|
|
45
48
|
return [];
|
|
@@ -54,11 +57,14 @@ function claudePrompt(entry: Record<string, unknown>): string | null {
|
|
|
54
57
|
if (CLAUDE_BOOKKEEPING.has(String(entry.type)) || entry.type !== 'user' || entry.isSidechain === true || entry.isMeta === true) {
|
|
55
58
|
return null;
|
|
56
59
|
}
|
|
60
|
+
|
|
57
61
|
const message = typeof entry.message === 'object' && entry.message !== null ? (entry.message as Record<string, unknown>) : {};
|
|
58
62
|
const said = spokenText(message.content);
|
|
63
|
+
|
|
59
64
|
if (said === null || said.trim() === '' || said.startsWith('<command-') || said.startsWith('<local-command-')) {
|
|
60
65
|
return null;
|
|
61
66
|
}
|
|
67
|
+
|
|
62
68
|
return said;
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -71,30 +77,38 @@ export async function listClaudeSessions({ projectsDir, folder, fs }: { projects
|
|
|
71
77
|
const dir = join(projectsDir, claudeProjectSlug(folder));
|
|
72
78
|
const names = (await fs.readdir(dir)).filter((name) => name.endsWith('.jsonl'));
|
|
73
79
|
const stats = await Promise.all(names.map(async (name) => ({ name, stat: await fs.stat(join(dir, name)) })));
|
|
80
|
+
|
|
74
81
|
const newest = stats
|
|
75
82
|
.flatMap(({ name, stat }) => (stat === null ? [] : [{ name, mtimeMs: stat.mtimeMs }]))
|
|
76
83
|
.sort((left, right) => right.mtimeMs - left.mtimeMs)
|
|
77
84
|
.slice(0, CLAUDE_MAX_FILES);
|
|
85
|
+
|
|
78
86
|
const sessions: ExternalSession[] = [];
|
|
87
|
+
|
|
79
88
|
for (const { name, mtimeMs } of newest) {
|
|
80
89
|
const prompt = records(await fs.readHead(join(dir, name), HEAD_BYTES))
|
|
81
90
|
.map(claudePrompt)
|
|
82
91
|
.find((said) => said !== null);
|
|
92
|
+
|
|
83
93
|
if (prompt !== undefined && prompt !== null) {
|
|
84
94
|
sessions.push({ provider: 'claude', sessionId: name.slice(0, -'.jsonl'.length), title: titleOf(prompt), updatedAt: mtimeMs, cwd: folder });
|
|
85
95
|
}
|
|
86
96
|
}
|
|
97
|
+
|
|
87
98
|
return sessions;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
function codexPrompt(entry: Record<string, unknown>): string | null {
|
|
91
102
|
const payload = typeof entry.payload === 'object' && entry.payload !== null ? (entry.payload as Record<string, unknown>) : null;
|
|
103
|
+
|
|
92
104
|
if (payload === null) {
|
|
93
105
|
return null;
|
|
94
106
|
}
|
|
107
|
+
|
|
95
108
|
if (entry.type === 'event_msg' && payload.type === 'user_message' && typeof payload.message === 'string') {
|
|
96
109
|
return payload.message.trim() === '' ? null : payload.message;
|
|
97
110
|
}
|
|
111
|
+
|
|
98
112
|
return null;
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -105,14 +119,17 @@ function newestFirst(names: string[]): string[] {
|
|
|
105
119
|
|
|
106
120
|
async function rolloutFiles(sessionsDir: string, fs: SessionFs, max: number): Promise<string[]> {
|
|
107
121
|
const files: string[] = [];
|
|
122
|
+
|
|
108
123
|
for (const year of newestFirst(await fs.readdir(sessionsDir))) {
|
|
109
124
|
for (const month of newestFirst(await fs.readdir(join(sessionsDir, year)))) {
|
|
110
125
|
for (const day of newestFirst(await fs.readdir(join(sessionsDir, year, month)))) {
|
|
111
126
|
const names = newestFirst(
|
|
112
127
|
(await fs.readdir(join(sessionsDir, year, month, day))).filter((name) => name.startsWith('rollout-') && name.endsWith('.jsonl'))
|
|
113
128
|
);
|
|
129
|
+
|
|
114
130
|
for (const name of names) {
|
|
115
131
|
files.push(join(sessionsDir, year, month, day, name));
|
|
132
|
+
|
|
116
133
|
if (files.length >= max) {
|
|
117
134
|
return files;
|
|
118
135
|
}
|
|
@@ -120,6 +137,7 @@ async function rolloutFiles(sessionsDir: string, fs: SessionFs, max: number): Pr
|
|
|
120
137
|
}
|
|
121
138
|
}
|
|
122
139
|
}
|
|
140
|
+
|
|
123
141
|
return files;
|
|
124
142
|
}
|
|
125
143
|
|
|
@@ -139,35 +157,45 @@ export async function listCodexSessions({
|
|
|
139
157
|
maxFiles?: number;
|
|
140
158
|
}): Promise<ExternalSession[]> {
|
|
141
159
|
const sessions: ExternalSession[] = [];
|
|
160
|
+
|
|
142
161
|
for (const path of await rolloutFiles(sessionsDir, fs, maxFiles)) {
|
|
143
162
|
const entries = records(await fs.readHead(path, HEAD_BYTES));
|
|
144
163
|
const meta = entries[0];
|
|
164
|
+
|
|
145
165
|
const payload =
|
|
146
166
|
meta?.type === 'session_meta' && typeof meta.payload === 'object' && meta.payload !== null ? (meta.payload as Record<string, unknown>) : null;
|
|
167
|
+
|
|
147
168
|
if (payload === null || payload.cwd !== folder || typeof payload.id !== 'string' || isForkOrSubagent(payload)) {
|
|
148
169
|
continue;
|
|
149
170
|
}
|
|
171
|
+
|
|
150
172
|
const prompt = entries.map(codexPrompt).find((said) => said !== null);
|
|
173
|
+
|
|
151
174
|
if (prompt === undefined || prompt === null) {
|
|
152
175
|
continue;
|
|
153
176
|
}
|
|
177
|
+
|
|
154
178
|
const stat = await fs.stat(path);
|
|
155
179
|
sessions.push({ provider: 'codex', sessionId: payload.id, title: titleOf(prompt), updatedAt: stat?.mtimeMs ?? 0, cwd: folder });
|
|
156
180
|
}
|
|
181
|
+
|
|
157
182
|
return sessions;
|
|
158
183
|
}
|
|
159
184
|
|
|
160
185
|
/* A `node:fs` behind the seam, for a host that lists the real thing. */
|
|
161
186
|
export async function nodeSessionFs(): Promise<SessionFs> {
|
|
162
187
|
const { readdir, stat, open } = await import('node:fs/promises');
|
|
188
|
+
|
|
163
189
|
return {
|
|
164
190
|
readdir: async (path) => readdir(path).catch(() => []),
|
|
165
191
|
stat: async (path) => stat(path).catch(() => null),
|
|
166
192
|
readHead: async (path, maxBytes) => {
|
|
167
193
|
const handle = await open(path, 'r');
|
|
194
|
+
|
|
168
195
|
try {
|
|
169
196
|
const buffer = Buffer.alloc(maxBytes);
|
|
170
197
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
198
|
+
|
|
171
199
|
return buffer.subarray(0, bytesRead).toString('utf8');
|
|
172
200
|
} finally {
|
|
173
201
|
await handle.close();
|
|
@@ -16,9 +16,11 @@ function imageInfo(bytes: Buffer): ImageInfo | null {
|
|
|
16
16
|
) {
|
|
17
17
|
return { mime: 'image/png', extension: 'png', width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
|
|
18
18
|
}
|
|
19
|
+
|
|
19
20
|
if (bytes.length >= 13 && /GIF8[79]a/.test(bytes.toString('ascii', 0, 6))) {
|
|
20
21
|
return { mime: 'image/gif', extension: 'gif', width: bytes.readUInt16LE(6), height: bytes.readUInt16LE(8) };
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
if (
|
|
23
25
|
bytes.length >= 20 &&
|
|
24
26
|
bytes.toString('ascii', 0, 4) === 'RIFF' &&
|
|
@@ -26,56 +28,77 @@ function imageInfo(bytes: Buffer): ImageInfo | null {
|
|
|
26
28
|
bytes.readUInt32LE(4) + 8 === bytes.length
|
|
27
29
|
) {
|
|
28
30
|
const kind = bytes.toString('ascii', 12, 16);
|
|
31
|
+
|
|
29
32
|
if (kind === 'VP8X' && bytes.length >= 30) {
|
|
30
33
|
return { mime: 'image/webp', extension: 'webp', width: bytes.readUIntLE(24, 3) + 1, height: bytes.readUIntLE(27, 3) + 1 };
|
|
31
34
|
}
|
|
35
|
+
|
|
32
36
|
if (kind === 'VP8 ' && bytes.length >= 30 && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
|
|
33
37
|
return { mime: 'image/webp', extension: 'webp', width: bytes.readUInt16LE(26) & 0x3fff, height: bytes.readUInt16LE(28) & 0x3fff };
|
|
34
38
|
}
|
|
39
|
+
|
|
35
40
|
if (kind === 'VP8L' && bytes.length >= 25 && bytes[20] === 0x2f) {
|
|
36
41
|
const size = bytes.readUInt32LE(21);
|
|
42
|
+
|
|
37
43
|
return { mime: 'image/webp', extension: 'webp', width: (size & 0x3fff) + 1, height: ((size >> 14) & 0x3fff) + 1 };
|
|
38
44
|
}
|
|
45
|
+
|
|
39
46
|
return null;
|
|
40
47
|
}
|
|
48
|
+
|
|
41
49
|
if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
|
|
42
50
|
let offset = 2;
|
|
51
|
+
|
|
43
52
|
while (offset + 4 <= bytes.length && bytes[offset] === 255) {
|
|
44
53
|
const marker = bytes[offset + 1]!;
|
|
54
|
+
|
|
45
55
|
if (marker === 218 || marker === 217) {
|
|
46
56
|
break;
|
|
47
57
|
}
|
|
58
|
+
|
|
48
59
|
if (marker === 255) {
|
|
49
60
|
offset++;
|
|
61
|
+
|
|
50
62
|
continue;
|
|
51
63
|
}
|
|
64
|
+
|
|
52
65
|
const size = bytes.readUInt16BE(offset + 2);
|
|
66
|
+
|
|
53
67
|
if (size < 2 || offset + 2 + size > bytes.length) {
|
|
54
68
|
return null;
|
|
55
69
|
}
|
|
70
|
+
|
|
56
71
|
if ([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207].includes(marker) && size >= 8) {
|
|
57
72
|
return { mime: 'image/jpeg', extension: 'jpg', width: bytes.readUInt16BE(offset + 7), height: bytes.readUInt16BE(offset + 5) };
|
|
58
73
|
}
|
|
74
|
+
|
|
59
75
|
offset += size + 2;
|
|
60
76
|
}
|
|
61
77
|
}
|
|
78
|
+
|
|
62
79
|
return null;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
82
|
export function generatedImage(data: string, prompt?: string): { upload: ChatAttachmentUpload; width?: number; height?: number } {
|
|
66
83
|
const parsed = ChatAttachmentUploadSchema.safeParse({ name: 'image', mime: 'application/octet-stream', data });
|
|
84
|
+
|
|
67
85
|
if (!parsed.success) {
|
|
68
86
|
throw new Error('The generated image is empty, invalid base64 or exceeds 10 MiB');
|
|
69
87
|
}
|
|
88
|
+
|
|
70
89
|
const bytes = Buffer.from(data, 'base64');
|
|
90
|
+
|
|
71
91
|
// Buffer's decoder accepts malformed padding and trailing bits; the protocol requires canonical base64.
|
|
72
92
|
if (bytes.toString('base64') !== data) {
|
|
73
93
|
throw new Error('The generated image contains invalid base64');
|
|
74
94
|
}
|
|
95
|
+
|
|
75
96
|
const info = imageInfo(bytes);
|
|
97
|
+
|
|
76
98
|
if (info === null || (info.width !== undefined && info.width === 0) || (info.height !== undefined && info.height === 0)) {
|
|
77
99
|
throw new Error('The generated bytes are not a supported image');
|
|
78
100
|
}
|
|
101
|
+
|
|
79
102
|
const stem =
|
|
80
103
|
(prompt ?? '')
|
|
81
104
|
.normalize('NFKD')
|
|
@@ -83,6 +106,8 @@ export function generatedImage(data: string, prompt?: string): { upload: ChatAtt
|
|
|
83
106
|
.replace(/^-|-$/g, '')
|
|
84
107
|
.slice(0, 80)
|
|
85
108
|
.toLowerCase() || 'generated-image';
|
|
109
|
+
|
|
86
110
|
const upload = { name: `${stem}.${info.extension}`, mime: info.mime, data };
|
|
111
|
+
|
|
87
112
|
return { upload, ...(info.width === undefined ? {} : { width: info.width }), ...(info.height === undefined ? {} : { height: info.height }) };
|
|
88
113
|
}
|
package/src/chat/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
2
|
export { type AcpAgentSpec, type AcpClientInfo, type AcpUsageReport, createAcpBackend } from './acp-backend.ts';
|
|
3
|
+
|
|
3
4
|
export type {
|
|
4
5
|
AcpAgentInfo,
|
|
5
6
|
AcpModel,
|
|
@@ -13,12 +14,14 @@ export type {
|
|
|
13
14
|
AcpThoughtLevel,
|
|
14
15
|
AcpTurnUsage
|
|
15
16
|
} from './acp-protocol.ts';
|
|
17
|
+
|
|
16
18
|
export { AttachmentStore } from './attachment-store.ts';
|
|
17
19
|
export type { BackendLaunch, TurnInput, ApprovalDecision, BackendEvent, BackendHost, ChatBackend } from './backend.ts';
|
|
18
20
|
export { runningInBackground, type BackgroundWork } from './background-work.ts';
|
|
19
21
|
export { BookmarkStore, type BookmarkListener } from './bookmark-store.ts';
|
|
20
22
|
export { type InterruptedRun, type ChatCoreHooks, type ChatCoreOptions, ChatCore } from './chat-core.ts';
|
|
21
23
|
export { COMPACT_ABOVE_BYTES, type ChatLogLine } from './chat-log.ts';
|
|
24
|
+
|
|
22
25
|
export {
|
|
23
26
|
type ChatSpawnOptions,
|
|
24
27
|
type SpawnChatProcess,
|
|
@@ -29,6 +32,7 @@ export {
|
|
|
29
32
|
type ChatProcess,
|
|
30
33
|
type ChatChildOptions
|
|
31
34
|
} from './chat-process.ts';
|
|
35
|
+
|
|
32
36
|
export {
|
|
33
37
|
type PromptNotes,
|
|
34
38
|
type ChatReferences,
|
|
@@ -40,6 +44,7 @@ export {
|
|
|
40
44
|
type ChatSendExtras,
|
|
41
45
|
type ResumeDecision
|
|
42
46
|
} from './chat-session.ts';
|
|
47
|
+
|
|
43
48
|
export { type ChatRecordExtras, type ChatRecord, ChatStore, type ChatSeq, type ChatStoreOptions } from './chat-store.ts';
|
|
44
49
|
export { suggestChatTitle, type ChatTitleInput } from './chat-title.ts';
|
|
45
50
|
export type { ClaudeBackendOptions } from './claude-backend.ts';
|
package/src/chat/input.ts
CHANGED
|
@@ -22,27 +22,34 @@ function isTokenEnd(char: string | undefined): boolean {
|
|
|
22
22
|
function findSkillToken(text: string, name: string, from: number): number {
|
|
23
23
|
const token = `$${name}`;
|
|
24
24
|
let index = text.indexOf(token, from);
|
|
25
|
+
|
|
25
26
|
while (index >= 0) {
|
|
26
27
|
if (isBoundary(text[index - 1]) && isTokenEnd(text[index + token.length])) {
|
|
27
28
|
return index;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
index = text.indexOf(token, index + 1);
|
|
30
32
|
}
|
|
33
|
+
|
|
31
34
|
return -1;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/* The last `$name` in the text that names one of the skills, longest name first on a tie. */
|
|
35
38
|
export function lastSkillToken(text: string, skills: string[]): { index: number; name: string } | null {
|
|
36
39
|
let best: { index: number; name: string } | null = null;
|
|
40
|
+
|
|
37
41
|
for (const name of [...new Set(skills)].sort((a, b) => b.length - a.length)) {
|
|
38
42
|
let index = findSkillToken(text, name, 0);
|
|
43
|
+
|
|
39
44
|
while (index >= 0) {
|
|
40
45
|
if (best === null || index > best.index) {
|
|
41
46
|
best = { index, name };
|
|
42
47
|
}
|
|
48
|
+
|
|
43
49
|
index = findSkillToken(text, name, index + 1);
|
|
44
50
|
}
|
|
45
51
|
}
|
|
52
|
+
|
|
46
53
|
return best;
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -55,11 +62,14 @@ export function lastSkillToken(text: string, skills: string[]): { index: number;
|
|
|
55
62
|
*/
|
|
56
63
|
export function splitSkillPrompt(text: string, skills: string[]): { lead: string; invocation: string | null } {
|
|
57
64
|
const last = lastSkillToken(text, skills);
|
|
65
|
+
|
|
58
66
|
if (last === null) {
|
|
59
67
|
return { lead: text, invocation: null };
|
|
60
68
|
}
|
|
69
|
+
|
|
61
70
|
const lead = text.slice(0, last.index).replace(/\$([A-Za-z][A-Za-z0-9_:-]*)/g, (match, name: string) => (skills.includes(name) ? `/${name}` : match));
|
|
62
71
|
const rest = text.slice(last.index + last.name.length + 1).replace(/^[ \t]+/, '');
|
|
72
|
+
|
|
63
73
|
return { lead: lead.trimEnd(), invocation: rest === '' ? `/${last.name}` : `/${last.name} ${rest}` };
|
|
64
74
|
}
|
|
65
75
|
|
|
@@ -79,11 +89,14 @@ export function buildUserMessage({ text, attachments = [], prefix = '', skills =
|
|
|
79
89
|
const note = attachmentNote(attachments);
|
|
80
90
|
// The prefix must be the first thing the model reads, so it joins the leading block.
|
|
81
91
|
const prompt = [`${prefix}${lead}`.trimEnd(), note].filter((part) => part.trim() !== '').join('\n\n');
|
|
92
|
+
|
|
82
93
|
if (prompt !== '') {
|
|
83
94
|
content.push({ type: 'text', text: prompt });
|
|
84
95
|
}
|
|
96
|
+
|
|
85
97
|
if (invocation !== null) {
|
|
86
98
|
content.push({ type: 'text', text: invocation });
|
|
87
99
|
}
|
|
100
|
+
|
|
88
101
|
return { type: 'user', message: { role: 'user', content }, parent_tool_use_id: null, session_id: '', ...(promptId ? { uuid: promptId } : {}) };
|
|
89
102
|
}
|