cli-copilot-worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/cli-copilot-worker.mjs +3 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +682 -0
  6. package/dist/src/cli.js.map +1 -0
  7. package/dist/src/core/copilot.d.ts +13 -0
  8. package/dist/src/core/copilot.js +56 -0
  9. package/dist/src/core/copilot.js.map +1 -0
  10. package/dist/src/core/failure-classifier.d.ts +8 -0
  11. package/dist/src/core/failure-classifier.js +148 -0
  12. package/dist/src/core/failure-classifier.js.map +1 -0
  13. package/dist/src/core/ids.d.ts +1 -0
  14. package/dist/src/core/ids.js +11 -0
  15. package/dist/src/core/ids.js.map +1 -0
  16. package/dist/src/core/markdown.d.ts +5 -0
  17. package/dist/src/core/markdown.js +14 -0
  18. package/dist/src/core/markdown.js.map +1 -0
  19. package/dist/src/core/paths.d.ts +18 -0
  20. package/dist/src/core/paths.js +42 -0
  21. package/dist/src/core/paths.js.map +1 -0
  22. package/dist/src/core/profile-faults.d.ts +15 -0
  23. package/dist/src/core/profile-faults.js +110 -0
  24. package/dist/src/core/profile-faults.js.map +1 -0
  25. package/dist/src/core/profile-manager.d.ts +25 -0
  26. package/dist/src/core/profile-manager.js +162 -0
  27. package/dist/src/core/profile-manager.js.map +1 -0
  28. package/dist/src/core/question-registry.d.ts +25 -0
  29. package/dist/src/core/question-registry.js +154 -0
  30. package/dist/src/core/question-registry.js.map +1 -0
  31. package/dist/src/core/store.d.ts +39 -0
  32. package/dist/src/core/store.js +206 -0
  33. package/dist/src/core/store.js.map +1 -0
  34. package/dist/src/core/types.d.ts +152 -0
  35. package/dist/src/core/types.js +2 -0
  36. package/dist/src/core/types.js.map +1 -0
  37. package/dist/src/daemon/client.d.ts +6 -0
  38. package/dist/src/daemon/client.js +117 -0
  39. package/dist/src/daemon/client.js.map +1 -0
  40. package/dist/src/daemon/server.d.ts +1 -0
  41. package/dist/src/daemon/server.js +149 -0
  42. package/dist/src/daemon/server.js.map +1 -0
  43. package/dist/src/daemon/service.d.ts +69 -0
  44. package/dist/src/daemon/service.js +800 -0
  45. package/dist/src/daemon/service.js.map +1 -0
  46. package/dist/src/doctor.d.ts +1 -0
  47. package/dist/src/doctor.js +74 -0
  48. package/dist/src/doctor.js.map +1 -0
  49. package/dist/src/index.d.ts +3 -0
  50. package/dist/src/index.js +4 -0
  51. package/dist/src/index.js.map +1 -0
  52. package/dist/src/output.d.ts +28 -0
  53. package/dist/src/output.js +307 -0
  54. package/dist/src/output.js.map +1 -0
  55. package/package.json +59 -0
  56. package/src/cli.ts +881 -0
  57. package/src/core/copilot.ts +75 -0
  58. package/src/core/failure-classifier.ts +202 -0
  59. package/src/core/ids.ts +11 -0
  60. package/src/core/markdown.ts +19 -0
  61. package/src/core/paths.ts +56 -0
  62. package/src/core/profile-faults.ts +140 -0
  63. package/src/core/profile-manager.ts +220 -0
  64. package/src/core/question-registry.ts +191 -0
  65. package/src/core/store.ts +273 -0
  66. package/src/core/types.ts +211 -0
  67. package/src/daemon/client.ts +137 -0
  68. package/src/daemon/server.ts +167 -0
  69. package/src/daemon/service.ts +968 -0
  70. package/src/doctor.ts +82 -0
  71. package/src/index.ts +3 -0
  72. package/src/output.ts +391 -0
@@ -0,0 +1,800 @@
1
+ import { unlink, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { approveAll } from '@github/copilot-sdk';
4
+ import { buildAuthenticatedClient } from '../core/copilot.js';
5
+ import { classifyCopilotStartupFailure, classifySessionErrorEvent } from '../core/failure-classifier.js';
6
+ import { ensureStateRoot } from '../core/paths.js';
7
+ import { ProfileFaultPlanner } from '../core/profile-faults.js';
8
+ import { ProfileManager } from '../core/profile-manager.js';
9
+ import { QuestionRegistry } from '../core/question-registry.js';
10
+ import { PersistentStore } from '../core/store.js';
11
+ const DEFAULT_MODEL = 'gpt-5.4';
12
+ const DEFAULT_TIMEOUT_MS = 300_000;
13
+ function estimateTokens(text) {
14
+ return Math.ceil(text.length / 4);
15
+ }
16
+ function applyDetail(entry, detail) {
17
+ if (detail === 'full') {
18
+ return entry;
19
+ }
20
+ if (detail === 'meta') {
21
+ return { ...entry, content: '' };
22
+ }
23
+ const limit = detail === 'verbose' ? 2000 : 500;
24
+ return {
25
+ ...entry,
26
+ content: entry.content.length > limit
27
+ ? `${entry.content.slice(0, limit)}...`
28
+ : entry.content,
29
+ };
30
+ }
31
+ export class CliCopilotWorkerService {
32
+ store = new PersistentStore();
33
+ activeExecutions = new Map();
34
+ clients = new Map();
35
+ profileManager;
36
+ faultPlanner;
37
+ createAuthenticatedClient = buildAuthenticatedClient;
38
+ questionRegistry = new QuestionRegistry(this.store, (conversationId, event, data) => {
39
+ const execution = this.activeExecutions.get(conversationId);
40
+ execution?.writer?.event(event, data);
41
+ if (event === 'question') {
42
+ execution?.signalWaitingAnswer?.();
43
+ execution?.resolveDone({
44
+ status: 'waiting_answer',
45
+ job: this.store.getJob(execution.jobId),
46
+ conversation: this.store.getConversation(conversationId),
47
+ });
48
+ }
49
+ });
50
+ async initialize() {
51
+ await this.store.load();
52
+ this.profileManager = ProfileManager.fromEnvironment(this.store.getProfiles());
53
+ this.faultPlanner = ProfileFaultPlanner.fromEnvironment();
54
+ this.store.setProfiles(this.profileManager.toPersistedState());
55
+ await this.store.persist();
56
+ }
57
+ async ping() {
58
+ return {
59
+ status: 'ok',
60
+ conversations: this.store.listConversations().length,
61
+ jobs: this.store.listJobs().length,
62
+ };
63
+ }
64
+ async daemonStatus() {
65
+ const { daemonMetaPath, socketPath } = ensureStateRoot();
66
+ return {
67
+ status: 'running',
68
+ socketPath,
69
+ daemonMetaPath,
70
+ profiles: this.profileManager.getProfiles(),
71
+ conversations: this.store.listConversations().length,
72
+ running: this.store.listJobs().filter((job) => job.status === 'running' || job.status === 'waiting_answer').length,
73
+ };
74
+ }
75
+ async shutdown() {
76
+ for (const execution of this.activeExecutions.values()) {
77
+ await execution.session.abort().catch(() => { });
78
+ await execution.session.disconnect().catch(() => { });
79
+ await execution.client.stop().catch(async () => {
80
+ await execution.client.forceStop().catch(() => { });
81
+ });
82
+ }
83
+ this.activeExecutions.clear();
84
+ for (const client of this.clients.values()) {
85
+ await client.stop().catch(async () => {
86
+ await client.forceStop().catch(() => { });
87
+ });
88
+ }
89
+ this.clients.clear();
90
+ const { daemonMetaPath, socketPath } = ensureStateRoot();
91
+ if (existsSync(daemonMetaPath)) {
92
+ await unlink(daemonMetaPath).catch(() => { });
93
+ }
94
+ if (existsSync(socketPath)) {
95
+ await unlink(socketPath).catch(() => { });
96
+ }
97
+ return { status: 'stopped' };
98
+ }
99
+ async run(args, writer) {
100
+ const conversation = this.store.createConversation({
101
+ cwd: args.cwd,
102
+ model: args.model ?? DEFAULT_MODEL,
103
+ });
104
+ const job = this.store.createJob({
105
+ conversationId: conversation.id,
106
+ kind: 'run',
107
+ inputFilePath: args.inputFilePath,
108
+ });
109
+ await this.persistState();
110
+ return await this.executeJob({
111
+ conversation,
112
+ job,
113
+ content: args.content,
114
+ async: args.async ?? false,
115
+ timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
116
+ writer,
117
+ });
118
+ }
119
+ async send(args, writer) {
120
+ const conversation = this.store.resolveConversation(args.conversationId);
121
+ if (!conversation) {
122
+ throw new Error(`Conversation not found: ${args.conversationId}`);
123
+ }
124
+ if (conversation.status === 'running' || conversation.status === 'waiting_answer') {
125
+ throw new Error(`Conversation ${conversation.id} is busy`);
126
+ }
127
+ const job = this.store.createJob({
128
+ conversationId: conversation.id,
129
+ kind: 'send',
130
+ inputFilePath: args.inputFilePath,
131
+ });
132
+ await this.persistState();
133
+ return await this.executeJob({
134
+ conversation,
135
+ job,
136
+ content: args.content,
137
+ async: args.async ?? false,
138
+ timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
139
+ writer,
140
+ });
141
+ }
142
+ async answer(args) {
143
+ const conversation = this.store.resolveConversation(args.conversationId);
144
+ if (!conversation) {
145
+ throw new Error(`Conversation not found: ${args.conversationId}`);
146
+ }
147
+ const result = this.questionRegistry.submitAnswer(conversation.id, args.content);
148
+ if (!result.success) {
149
+ throw new Error(result.error);
150
+ }
151
+ await this.persistState();
152
+ return {
153
+ conversationId: conversation.id,
154
+ answer: result.resolvedAnswer,
155
+ status: 'submitted',
156
+ };
157
+ }
158
+ async read(args) {
159
+ const conversation = this.store.resolveConversation(args.conversationId);
160
+ if (!conversation) {
161
+ throw new Error(`Conversation not found: ${args.conversationId}`);
162
+ }
163
+ const lastJob = conversation.lastJobId ? this.store.getJob(conversation.lastJobId) : undefined;
164
+ const transcript = await this.store.readTranscript(conversation.id);
165
+ const detail = args.detail ?? 'standard';
166
+ let entries = transcript;
167
+ if (args.after !== undefined) {
168
+ entries = entries.filter((entry) => entry.index > args.after);
169
+ }
170
+ if (args.before !== undefined) {
171
+ entries = entries.filter((entry) => entry.index < args.before);
172
+ }
173
+ const start = args.from ?? (entries[0]?.index ?? 1);
174
+ const end = args.to ?? (entries[entries.length - 1]?.index ?? start);
175
+ entries = entries.filter((entry) => entry.index >= start && entry.index <= end);
176
+ const budget = args.tokens ?? 4000;
177
+ let spent = 0;
178
+ const sliced = [];
179
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
180
+ const entry = applyDetail(entries[index], detail);
181
+ const cost = estimateTokens(entry.content || JSON.stringify(entry.data ?? {})) + 4;
182
+ if (sliced.length > 0 && spent + cost > budget) {
183
+ break;
184
+ }
185
+ sliced.unshift(entry);
186
+ spent += cost;
187
+ }
188
+ return {
189
+ conversation: {
190
+ id: conversation.id,
191
+ status: conversation.status,
192
+ cwd: conversation.cwd,
193
+ model: conversation.model,
194
+ pendingQuestion: conversation.pendingQuestion,
195
+ lastJobId: conversation.lastJobId,
196
+ },
197
+ meta: {
198
+ totalEntries: transcript.length,
199
+ returnedEntries: sliced.length,
200
+ tokenBudget: budget,
201
+ approxTokens: spent,
202
+ nextActions: {
203
+ send: `cli-copilot-worker send ${conversation.id} prompt.md`,
204
+ answer: conversation.pendingQuestion ? `cli-copilot-worker answer ${conversation.id} answer.md` : null,
205
+ },
206
+ },
207
+ attempts: lastJob?.attempts ?? [],
208
+ entries: sliced,
209
+ };
210
+ }
211
+ async list() {
212
+ return {
213
+ conversations: this.store.listConversations().map((conversation) => ({
214
+ id: conversation.id,
215
+ status: conversation.status,
216
+ cwd: conversation.cwd,
217
+ model: conversation.model,
218
+ updatedAt: conversation.updatedAt,
219
+ pendingQuestion: conversation.pendingQuestion?.question,
220
+ })),
221
+ };
222
+ }
223
+ async info(conversationId) {
224
+ const conversation = this.store.resolveConversation(conversationId);
225
+ if (!conversation) {
226
+ throw new Error(`Conversation not found: ${conversationId}`);
227
+ }
228
+ const jobs = this.store.jobsForConversation(conversation.id);
229
+ return {
230
+ conversation,
231
+ jobs,
232
+ };
233
+ }
234
+ async jobList() {
235
+ return {
236
+ jobs: this.store.listJobs(),
237
+ };
238
+ }
239
+ async jobStatus(jobId) {
240
+ const job = this.store.getJob(jobId);
241
+ if (!job) {
242
+ throw new Error(`Job not found: ${jobId}`);
243
+ }
244
+ const conversation = this.store.getConversation(job.conversationId);
245
+ return {
246
+ job,
247
+ attempts: job.attempts,
248
+ activeProfileId: conversation?.profileId,
249
+ activeProfileConfigDir: conversation?.profileConfigDir,
250
+ actions: this.buildJobActions(job),
251
+ };
252
+ }
253
+ async jobRead(jobId) {
254
+ const job = this.store.getJob(jobId);
255
+ if (!job) {
256
+ throw new Error(`Job not found: ${jobId}`);
257
+ }
258
+ const conversation = this.store.getConversation(job.conversationId);
259
+ if (!conversation) {
260
+ throw new Error(`Conversation not found: ${job.conversationId}`);
261
+ }
262
+ const transcript = await this.store.readTranscript(conversation.id);
263
+ const entries = transcript.filter((entry) => {
264
+ if (entry.index < job.startEntryIndex) {
265
+ return false;
266
+ }
267
+ if (job.endEntryIndex !== undefined && entry.index > job.endEntryIndex) {
268
+ return false;
269
+ }
270
+ return true;
271
+ });
272
+ return {
273
+ job,
274
+ attempts: job.attempts,
275
+ activeProfileId: conversation.profileId,
276
+ activeProfileConfigDir: conversation.profileConfigDir,
277
+ entries,
278
+ };
279
+ }
280
+ async jobWait(args) {
281
+ const timeoutMs = (args.timeoutSeconds ?? 300) * 1000;
282
+ const intervalMs = (args.intervalSeconds ?? 2) * 1000;
283
+ const started = Date.now();
284
+ while (true) {
285
+ const job = this.store.getJob(args.jobId);
286
+ if (!job) {
287
+ throw new Error(`Job not found: ${args.jobId}`);
288
+ }
289
+ if (job.status !== 'running') {
290
+ return {
291
+ job,
292
+ actions: this.buildJobActions(job),
293
+ };
294
+ }
295
+ if (Date.now() - started > timeoutMs) {
296
+ throw new Error(`Job ${args.jobId} did not complete within ${args.timeoutSeconds ?? 300}s`);
297
+ }
298
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
299
+ }
300
+ }
301
+ async jobCancel(jobId) {
302
+ const job = this.store.getJob(jobId);
303
+ if (!job) {
304
+ throw new Error(`Job not found: ${jobId}`);
305
+ }
306
+ const execution = this.activeExecutions.get(job.conversationId);
307
+ if (execution) {
308
+ if (execution.attempt.outcome === 'running') {
309
+ this.store.finalizeJobAttempt(execution.attempt, {
310
+ outcome: 'failed',
311
+ endedAt: new Date().toISOString(),
312
+ failureMessage: 'Job cancelled.',
313
+ });
314
+ }
315
+ await execution.session.abort().catch(() => { });
316
+ await execution.session.disconnect().catch(() => { });
317
+ await execution.client.stop().catch(async () => {
318
+ await execution.client.forceStop().catch(() => { });
319
+ });
320
+ this.activeExecutions.delete(job.conversationId);
321
+ }
322
+ this.store.finalizeJob(job.id, 'cancelled');
323
+ this.store.updateConversation(job.conversationId, {
324
+ status: 'cancelled',
325
+ });
326
+ await this.store.appendTranscript({
327
+ conversationId: job.conversationId,
328
+ jobId: job.id,
329
+ role: 'status',
330
+ content: 'Job cancelled.',
331
+ });
332
+ await this.persistState();
333
+ return {
334
+ job: this.store.getJob(job.id),
335
+ };
336
+ }
337
+ async executeJob(input) {
338
+ const completion = this.startExecution(input);
339
+ if (input.async) {
340
+ return {
341
+ conversationId: input.conversation.id,
342
+ jobId: input.job.id,
343
+ status: 'running',
344
+ attempts: input.job.attempts,
345
+ activeProfileId: input.conversation.profileId,
346
+ activeProfileConfigDir: input.conversation.profileConfigDir,
347
+ actions: this.buildJobActions(input.job),
348
+ };
349
+ }
350
+ const result = await completion;
351
+ return {
352
+ conversationId: result.conversation.id,
353
+ jobId: result.job.id,
354
+ status: result.status,
355
+ attempts: result.job.attempts,
356
+ activeProfileId: result.conversation.profileId,
357
+ activeProfileConfigDir: result.conversation.profileConfigDir,
358
+ pendingQuestion: result.conversation.pendingQuestion,
359
+ actions: this.buildConversationActions(result.conversation),
360
+ };
361
+ }
362
+ startExecution(input) {
363
+ const done = new Promise((resolve, reject) => {
364
+ void this.executeInBackground(input, resolve, reject);
365
+ });
366
+ return done;
367
+ }
368
+ async executeInBackground(input, resolveDone, rejectDone) {
369
+ const conversation = this.store.getConversation(input.conversation.id);
370
+ const job = this.store.getJob(input.job.id);
371
+ await this.store.appendTranscript({
372
+ conversationId: conversation.id,
373
+ jobId: job.id,
374
+ role: 'user',
375
+ content: input.content,
376
+ data: {
377
+ inputFilePath: job.inputFilePath,
378
+ kind: job.kind,
379
+ },
380
+ });
381
+ input.writer?.event('status', {
382
+ message: `Running ${job.kind} on ${conversation.id}`,
383
+ });
384
+ const profiles = this.planProfiles(conversation);
385
+ if (profiles.length === 0) {
386
+ const failure = classifyCopilotStartupFailure(new Error('No eligible Copilot profiles are available. All configured profiles are cooling down or unavailable.'));
387
+ await this.finalizeJobFailure(job.id, conversation.id, failure.message);
388
+ rejectDone(new Error(failure.message));
389
+ return;
390
+ }
391
+ for (let index = 0; index < profiles.length; index += 1) {
392
+ const profile = profiles[index];
393
+ const attempt = this.store.createJobAttempt(job.id, profile);
394
+ const attemptLabel = `profile ${profile.id} (${profile.configDir})`;
395
+ this.store.updateConversation(conversation.id, {
396
+ status: 'running',
397
+ profileId: profile.id,
398
+ profileConfigDir: profile.configDir,
399
+ });
400
+ await this.store.appendTranscript({
401
+ conversationId: conversation.id,
402
+ jobId: job.id,
403
+ role: 'status',
404
+ content: index === 0
405
+ ? `Using ${attemptLabel}.`
406
+ : `Switching to ${attemptLabel}.`,
407
+ });
408
+ input.writer?.event('status', {
409
+ message: index === 0
410
+ ? `Using ${attemptLabel}`
411
+ : `Switching to ${attemptLabel}`,
412
+ });
413
+ await this.persistState();
414
+ const outcome = await this.startProfileAttempt({
415
+ conversation,
416
+ job,
417
+ attempt,
418
+ profile,
419
+ content: input.content,
420
+ timeoutMs: input.timeoutMs,
421
+ writer: input.writer,
422
+ resolveDone,
423
+ rejectDone,
424
+ });
425
+ if (outcome.kind === 'completed' || outcome.kind === 'waiting_answer') {
426
+ return;
427
+ }
428
+ this.profileManager.markFailure(profile.id, outcome.failure.category, outcome.failure.message);
429
+ await this.persistState();
430
+ const nextProfile = profiles[index + 1];
431
+ if (outcome.failure.retryable && nextProfile) {
432
+ const switchMessage = `Profile ${profile.id} failed with ${outcome.failure.category}; switching to ${nextProfile.id}.`;
433
+ await this.store.appendTranscript({
434
+ conversationId: conversation.id,
435
+ jobId: job.id,
436
+ role: 'status',
437
+ content: switchMessage,
438
+ });
439
+ input.writer?.event('status', { message: switchMessage });
440
+ await this.persistState();
441
+ continue;
442
+ }
443
+ await this.finalizeJobFailure(job.id, conversation.id, outcome.failure.message);
444
+ rejectDone(new Error(outcome.failure.message));
445
+ return;
446
+ }
447
+ }
448
+ async startProfileAttempt(input) {
449
+ const injectedFault = this.faultPlanner.takeFault(input.profile);
450
+ if (injectedFault) {
451
+ const failure = classifySessionErrorEvent({
452
+ errorType: injectedFault.errorType,
453
+ message: injectedFault.message ?? `Injected ${injectedFault.category} failure`,
454
+ statusCode: injectedFault.statusCode,
455
+ });
456
+ await this.recordAttemptFailure({
457
+ conversationId: input.conversation.id,
458
+ jobId: input.job.id,
459
+ attempt: input.attempt,
460
+ failure,
461
+ });
462
+ input.writer?.event('error', { message: failure.message });
463
+ return { kind: 'failed', failure };
464
+ }
465
+ let client;
466
+ let clientKey;
467
+ try {
468
+ const clientResult = await this.getOrCreateClient(input.conversation.cwd, input.profile);
469
+ client = clientResult.client;
470
+ clientKey = clientResult.clientKey;
471
+ const session = input.conversation.hasStarted
472
+ ? await client.resumeSession(input.conversation.sessionId, {
473
+ onPermissionRequest: approveAll,
474
+ onUserInputRequest: (request, invocation) => this.questionRegistry.register({
475
+ conversationId: input.conversation.id,
476
+ jobId: input.job.id,
477
+ sessionId: invocation.sessionId,
478
+ question: request.question,
479
+ choices: request.choices,
480
+ allowFreeform: request.allowFreeform,
481
+ }),
482
+ streaming: true,
483
+ })
484
+ : await client.createSession({
485
+ sessionId: input.conversation.sessionId,
486
+ model: input.conversation.model,
487
+ onPermissionRequest: approveAll,
488
+ onUserInputRequest: (request, invocation) => this.questionRegistry.register({
489
+ conversationId: input.conversation.id,
490
+ jobId: input.job.id,
491
+ sessionId: invocation.sessionId,
492
+ question: request.question,
493
+ choices: request.choices,
494
+ allowFreeform: request.allowFreeform,
495
+ }),
496
+ streaming: true,
497
+ });
498
+ this.store.updateConversation(input.conversation.id, {
499
+ status: 'running',
500
+ hasStarted: true,
501
+ profileId: input.profile.id,
502
+ profileConfigDir: input.profile.configDir,
503
+ });
504
+ const activeClient = client;
505
+ const activeClientKey = clientKey;
506
+ return await new Promise((resolve) => {
507
+ let finished = false;
508
+ let waitingForAnswer = false;
509
+ let startResolved = false;
510
+ let timeout;
511
+ const resolveStart = (result) => {
512
+ if (startResolved) {
513
+ return;
514
+ }
515
+ startResolved = true;
516
+ resolve(result);
517
+ };
518
+ const cleanup = async (dropClient) => {
519
+ await session.disconnect().catch(() => { });
520
+ this.activeExecutions.delete(input.conversation.id);
521
+ if (dropClient) {
522
+ await activeClient.stop().catch(async () => {
523
+ await activeClient.forceStop().catch(() => { });
524
+ });
525
+ this.clients.delete(activeClientKey);
526
+ }
527
+ };
528
+ const finalizeFailure = async (failure, finalAfterQuestion) => {
529
+ if (finished) {
530
+ return;
531
+ }
532
+ finished = true;
533
+ if (timeout) {
534
+ clearTimeout(timeout);
535
+ }
536
+ input.writer?.event('error', { message: failure.message });
537
+ await this.recordAttemptFailure({
538
+ conversationId: input.conversation.id,
539
+ jobId: input.job.id,
540
+ attempt: input.attempt,
541
+ failure,
542
+ });
543
+ await cleanup(true);
544
+ if (finalAfterQuestion) {
545
+ await this.finalizeJobFailure(input.job.id, input.conversation.id, failure.message);
546
+ input.rejectDone(new Error(failure.message));
547
+ }
548
+ else {
549
+ resolveStart({ kind: 'failed', failure });
550
+ }
551
+ };
552
+ const finalizeSuccess = async () => {
553
+ if (finished) {
554
+ return;
555
+ }
556
+ finished = true;
557
+ if (timeout) {
558
+ clearTimeout(timeout);
559
+ }
560
+ this.profileManager.markSuccess(input.profile.id);
561
+ this.store.finalizeJobAttempt(input.attempt, {
562
+ outcome: 'completed',
563
+ endedAt: new Date().toISOString(),
564
+ });
565
+ const updatedJob = this.store.finalizeJob(input.job.id, 'completed');
566
+ updatedJob.endEntryIndex = this.store.getConversation(input.conversation.id)?.nextEntryIndex ?? updatedJob.endEntryIndex;
567
+ this.store.updateConversation(input.conversation.id, {
568
+ status: 'idle',
569
+ lastError: undefined,
570
+ profileId: input.profile.id,
571
+ profileConfigDir: input.profile.configDir,
572
+ });
573
+ await this.store.appendTranscript({
574
+ conversationId: input.conversation.id,
575
+ jobId: input.job.id,
576
+ role: 'status',
577
+ content: 'Session is idle.',
578
+ });
579
+ await this.persistState();
580
+ await cleanup(false);
581
+ input.resolveDone({
582
+ status: 'completed',
583
+ job: updatedJob,
584
+ conversation: this.store.getConversation(input.conversation.id),
585
+ });
586
+ resolveStart({ kind: 'completed' });
587
+ };
588
+ this.activeExecutions.set(input.conversation.id, {
589
+ conversationId: input.conversation.id,
590
+ jobId: input.job.id,
591
+ attempt: input.attempt,
592
+ profile: input.profile,
593
+ session,
594
+ client: activeClient,
595
+ writer: input.writer,
596
+ resolveDone: input.resolveDone,
597
+ rejectDone: input.rejectDone,
598
+ signalWaitingAnswer: () => {
599
+ waitingForAnswer = true;
600
+ resolveStart({ kind: 'waiting_answer' });
601
+ },
602
+ });
603
+ session.on('assistant.message_delta', (event) => {
604
+ input.writer?.event('assistant.delta', {
605
+ text: event.data.deltaContent,
606
+ });
607
+ });
608
+ session.on('assistant.message', (event) => {
609
+ void this.store.appendTranscript({
610
+ conversationId: input.conversation.id,
611
+ jobId: input.job.id,
612
+ role: 'assistant',
613
+ content: event.data.content,
614
+ });
615
+ });
616
+ session.on('tool.execution_start', (event) => {
617
+ input.writer?.event('tool.start', {
618
+ toolName: event.data.toolName,
619
+ });
620
+ void this.store.appendTranscript({
621
+ conversationId: input.conversation.id,
622
+ jobId: input.job.id,
623
+ role: 'tool_use',
624
+ content: event.data.toolName,
625
+ });
626
+ });
627
+ session.on('tool.execution_complete', (event) => {
628
+ const label = 'toolName' in event.data ? event.data.toolName : event.data.toolCallId;
629
+ input.writer?.event('tool.complete', {
630
+ toolName: label,
631
+ success: event.data.success,
632
+ });
633
+ void this.store.appendTranscript({
634
+ conversationId: input.conversation.id,
635
+ jobId: input.job.id,
636
+ role: 'tool_result',
637
+ content: `${label} success=${String(event.data.success)}`,
638
+ });
639
+ });
640
+ session.on('session.error', (event) => {
641
+ void finalizeFailure(classifySessionErrorEvent({
642
+ errorType: event.data.errorType,
643
+ message: event.data.message,
644
+ statusCode: event.data.statusCode,
645
+ }), waitingForAnswer);
646
+ });
647
+ session.on('session.idle', () => {
648
+ void finalizeSuccess();
649
+ });
650
+ timeout = setTimeout(() => {
651
+ void finalizeFailure(classifyCopilotStartupFailure(new Error(`Timed out after ${input.timeoutMs}ms.`)), waitingForAnswer);
652
+ }, input.timeoutMs);
653
+ timeout.unref();
654
+ void this.persistState()
655
+ .then(async () => {
656
+ await session.send({ prompt: input.content });
657
+ })
658
+ .catch((error) => {
659
+ void finalizeFailure(classifyCopilotStartupFailure(error), waitingForAnswer);
660
+ });
661
+ });
662
+ }
663
+ catch (error) {
664
+ const failure = classifyCopilotStartupFailure(error);
665
+ await this.recordAttemptFailure({
666
+ conversationId: input.conversation.id,
667
+ jobId: input.job.id,
668
+ attempt: input.attempt,
669
+ failure,
670
+ });
671
+ if (client && clientKey) {
672
+ await this.disposeClient(clientKey, client);
673
+ }
674
+ return { kind: 'failed', failure };
675
+ }
676
+ }
677
+ planProfiles(conversation) {
678
+ const eligible = this.profileManager.getCandidateProfiles();
679
+ if (!conversation.profileConfigDir && !conversation.profileId) {
680
+ return eligible;
681
+ }
682
+ const prioritized = [];
683
+ const seen = new Set();
684
+ const push = (profile) => {
685
+ if (!profile || seen.has(profile.configDir)) {
686
+ return;
687
+ }
688
+ prioritized.push(profile);
689
+ seen.add(profile.configDir);
690
+ };
691
+ push(eligible.find((profile) => ((conversation.profileId && profile.id === conversation.profileId)
692
+ || (conversation.profileConfigDir && profile.configDir === conversation.profileConfigDir))));
693
+ eligible.forEach((profile) => push(profile));
694
+ return prioritized;
695
+ }
696
+ async getOrCreateClient(cwd, profile) {
697
+ const clientKey = `${cwd}:${profile.configDir}`;
698
+ let client = this.clients.get(clientKey);
699
+ if (!client) {
700
+ client = await this.createAuthenticatedClient({
701
+ cwd,
702
+ profile,
703
+ });
704
+ this.clients.set(clientKey, client);
705
+ }
706
+ return { client, clientKey };
707
+ }
708
+ async disposeClient(clientKey, client) {
709
+ await client.stop().catch(async () => {
710
+ await client.forceStop().catch(() => { });
711
+ });
712
+ this.clients.delete(clientKey);
713
+ }
714
+ async recordAttemptFailure(input) {
715
+ this.store.finalizeJobAttempt(input.attempt, {
716
+ outcome: 'failed',
717
+ endedAt: new Date().toISOString(),
718
+ failureCategory: input.failure.category,
719
+ failureMessage: input.failure.message,
720
+ sessionErrorType: input.failure.sessionErrorType,
721
+ sessionStatusCode: input.failure.sessionStatusCode,
722
+ });
723
+ await this.store.appendTranscript({
724
+ conversationId: input.conversationId,
725
+ jobId: input.jobId,
726
+ role: 'error',
727
+ content: input.failure.message,
728
+ data: {
729
+ failureCategory: input.failure.category,
730
+ sessionErrorType: input.failure.sessionErrorType,
731
+ sessionStatusCode: input.failure.sessionStatusCode,
732
+ },
733
+ });
734
+ }
735
+ async finalizeJobFailure(jobId, conversationId, message) {
736
+ const updatedJob = this.store.finalizeJob(jobId, 'failed', message);
737
+ updatedJob.endEntryIndex = this.store.getConversation(conversationId)?.nextEntryIndex ?? updatedJob.endEntryIndex;
738
+ this.store.updateConversation(conversationId, {
739
+ status: 'failed',
740
+ lastError: message,
741
+ });
742
+ await this.store.appendTranscript({
743
+ conversationId,
744
+ jobId,
745
+ role: 'status',
746
+ content: `Job failed: ${message}`,
747
+ });
748
+ await this.persistState();
749
+ }
750
+ buildConversationActions(conversation) {
751
+ return {
752
+ read: `cli-copilot-worker read ${conversation.id}`,
753
+ send: conversation.status === 'idle' ? `cli-copilot-worker send ${conversation.id} prompt.md` : null,
754
+ answer: conversation.pendingQuestion ? `cli-copilot-worker answer ${conversation.id} answer.md` : null,
755
+ info: `cli-copilot-worker info ${conversation.id}`,
756
+ };
757
+ }
758
+ buildJobActions(job) {
759
+ return {
760
+ status: `cli-copilot-worker job status ${job.id}`,
761
+ wait: job.status === 'running' ? `cli-copilot-worker job wait ${job.id}` : null,
762
+ read: `cli-copilot-worker job read ${job.id}`,
763
+ answer: job.status === 'waiting_answer'
764
+ ? `cli-copilot-worker answer ${job.conversationId} answer.md`
765
+ : null,
766
+ cancel: job.status === 'running' || job.status === 'waiting_answer'
767
+ ? `cli-copilot-worker job cancel ${job.id}`
768
+ : null,
769
+ };
770
+ }
771
+ async writeDaemonMeta(socketPath, token) {
772
+ await writeFile(ensureStateRoot().daemonMetaPath, JSON.stringify({
773
+ pid: process.pid,
774
+ socketPath,
775
+ token,
776
+ startedAt: new Date().toISOString(),
777
+ }, null, 2));
778
+ }
779
+ async persistState() {
780
+ this.store.setProfiles(this.profileManager.toPersistedState());
781
+ await this.store.persist();
782
+ }
783
+ resolveProfile(conversation) {
784
+ if (conversation.profileConfigDir && conversation.profileId) {
785
+ const match = this.profileManager
786
+ .getProfiles()
787
+ .find((profile) => profile.id === conversation.profileId || profile.configDir === conversation.profileConfigDir);
788
+ if (match) {
789
+ return match;
790
+ }
791
+ return {
792
+ id: conversation.profileId,
793
+ configDir: conversation.profileConfigDir,
794
+ failureCount: 0,
795
+ };
796
+ }
797
+ return this.profileManager.getCurrentProfile();
798
+ }
799
+ }
800
+ //# sourceMappingURL=service.js.map