copilot-operator 0.1.7 → 0.1.8

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 (36) hide show
  1. package/dist/src/api/operator.service.js +15 -9
  2. package/dist/src/plan/brief.js +4 -4
  3. package/dist/src/plan/systemGuide.js +22 -6
  4. package/dist/src/session/compose.js +24 -19
  5. package/dist/src/session/interruption.js +104 -0
  6. package/dist/src/session/store.js +21 -5
  7. package/dist/src/session/story.js +1 -1
  8. package/dist/web/404.html +2 -2
  9. package/dist/web/_next/static/chunks/867-3d4ee08c5f9f0e71.js +1 -0
  10. package/dist/web/_next/static/chunks/app/defaults/{page-1747c931adc746fd.js → page-dfcbd748f454d2d7.js} +1 -1
  11. package/dist/web/_next/static/chunks/app/history/page-49413c3a8266a0b7.js +1 -0
  12. package/dist/web/_next/static/chunks/app/sessions/view/page-a82e5e801dabc423.js +1 -0
  13. package/dist/web/appearance.html +2 -2
  14. package/dist/web/appearance.txt +11 -11
  15. package/dist/web/defaults.html +2 -2
  16. package/dist/web/defaults.txt +11 -11
  17. package/dist/web/history.html +2 -2
  18. package/dist/web/history.txt +11 -11
  19. package/dist/web/import.html +2 -2
  20. package/dist/web/import.txt +11 -11
  21. package/dist/web/index.html +2 -2
  22. package/dist/web/index.txt +11 -11
  23. package/dist/web/level1.html +2 -2
  24. package/dist/web/level1.txt +11 -11
  25. package/dist/web/presets.html +2 -2
  26. package/dist/web/presets.txt +11 -11
  27. package/dist/web/sessions/view.html +2 -2
  28. package/dist/web/sessions/view.txt +11 -11
  29. package/dist/web/system.html +2 -2
  30. package/dist/web/system.txt +11 -11
  31. package/package.json +1 -1
  32. package/dist/web/_next/static/chunks/867-4aa80f5a32311bce.js +0 -1
  33. package/dist/web/_next/static/chunks/app/history/page-2251664c0abe3f11.js +0 -1
  34. package/dist/web/_next/static/chunks/app/sessions/view/page-9517dd74c6d00f85.js +0 -1
  35. /package/dist/web/_next/static/{DCpEMjhrg9APkRwBBw5Do → kysrkUS5Y-ZTHsIIXgs3q}/_buildManifest.js +0 -0
  36. /package/dist/web/_next/static/{DCpEMjhrg9APkRwBBw5Do → kysrkUS5Y-ZTHsIIXgs3q}/_ssgManifest.js +0 -0
@@ -36,6 +36,7 @@ function sameFolder(a, b) {
36
36
  return a.trim() !== '' && norm(a) === norm(b);
37
37
  }
38
38
  import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
39
+ import { readInterruption } from '../session/interruption.js';
39
40
  import { commitInterrupted, repoDirOf, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
40
41
  import { changedFilesBetween, fileAt, gitAvailable, repoUnusableReason } from '../vcs/git.js';
41
42
  import { makeAuthorizer } from '../exec/authorizer.js';
@@ -101,19 +102,23 @@ let OperatorService = class OperatorService {
101
102
  if (!session || !task)
102
103
  return;
103
104
  const vcs = await commitInterrupted(session, task, task.reason ?? 'interrupted', this.bus);
104
- const settled = vcs?.commit
105
- ? await this.store.updateSession(sessionId, (s) => {
106
- const t = s.tasks.find((x) => x.id === taskId);
107
- if (!t)
108
- return;
105
+ const cfg = await this.settings.load();
106
+ // Where it had got to, from its run folder, so "Continue" can tell the chat exactly that.
107
+ const interruption = task.runId ? await readInterruption(join(cfg.resolved.runsDir, task.runId)) : null;
108
+ const settled = await this.store.updateSession(sessionId, (s) => {
109
+ const t = s.tasks.find((x) => x.id === taskId);
110
+ if (!t)
111
+ return;
112
+ if (interruption)
113
+ t.interruption = interruption;
114
+ if (vcs?.commit) {
109
115
  t.vcs = vcs;
110
116
  t.reason = `${t.reason ?? ''} What it had changed is committed on ${vcs.branch} as ${vcs.commit?.slice(0, 8)}.`.trim();
111
- })
112
- : session;
117
+ }
118
+ });
113
119
  const ended = settled.tasks.find((t) => t.id === taskId);
114
120
  if (!ended)
115
121
  return;
116
- const cfg = await this.settings.load();
117
122
  await writeAttemptRecord(settled, ended, cfg.resolved.runsDir, exportMachine(cfg));
118
123
  }
119
124
  /** Runs the startup work now, so it does not wait for the first request. */
@@ -314,7 +319,7 @@ let OperatorService = class OperatorService {
314
319
  * the text it ran with, and only then is the new text applied. Editing in place would leave
315
320
  * the old attempt's summary sitting under a question that was never asked.
316
321
  */
317
- /** "Continue" on a task that stopped at the runner's limit: same chat, same branch, fresh count. */
322
+ /** "Continue" on a task that stopped before it finished — its limit, the bot stopping, the operator: same chat, same branch, fresh count. */
318
323
  async continueTask(sessionId, taskId) {
319
324
  await this.init();
320
325
  const task = await this.store.continueTask(sessionId, taskId);
@@ -2020,6 +2025,7 @@ let OperatorService = class OperatorService {
2020
2025
  autoRetries: t.autoRetries || undefined,
2021
2026
  attempt: t.attempt,
2022
2027
  changedFiles: t.vcs?.commit && t.vcs.baseCommit ? (t.vcs.files?.length ?? 0) : undefined,
2028
+ interrupted: t.status === 'aborted' && !!t.interruption ? true : undefined,
2023
2029
  attempts: (t.attempts ?? []).map((a, n) => {
2024
2030
  const from = a.startedAt ? Date.parse(a.startedAt) : undefined;
2025
2031
  const to = a.finishedAt ? Date.parse(a.finishedAt) : undefined;
@@ -726,7 +726,7 @@ in those words, every time:
726
726
  1. On \`/history\`, in the **"Flow"** view, press **"Fix the prompt and queue it again"** on that row.
727
727
  2. Replace the text with the one above and press **"Save and queue again"**.
728
728
  3. Under **"What is next"**, press **"Continue: run the {n} queued task(s) in {s} session(s)"**
729
- (it reads **"Continue: run {f} failed and {n} queued task(s) in {s} session(s)"** when a chain
729
+ (it reads **"Continue: run {f} unfinished and {n} queued task(s) in {s} session(s)"** when a chain
730
730
  has a failed task).
731
731
  4. In **"Before it continues"** every task is ticked. To run only the fixed one, press
732
732
  **"only this one"** beside it.
@@ -736,7 +736,7 @@ in those words, every time:
736
736
  A check that is wrong rather than a prompt that is wrong is edited instead: **"Edit"** on the
737
737
  task card, then **"What it checks"**, **"Must be"** and **"Command"** under **"Checks"**, then
738
738
  **"Save and queue it again"** — which, like every "queue" button, starts nothing by itself.
739
- A task that ended **limit-reached** is carried on in its own chat instead: **"Continue in the
739
+ A task that ended **limit-reached**, or **aborted** because the bot stopped under it, is carried on in its own chat instead: **"Continue in the
740
740
  same chat"** on its card on the session page, then **"Run {n} task(s)"** at the top of that page.
741
741
  When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
742
742
  while you work it out.
@@ -887,7 +887,7 @@ const PHASE3_BG = `
887
887
  1. В \`/history\`, в изгледа **„Поток“**, натисни **„Поправи prompt-а и върни в опашката“** на този ред.
888
888
  2. Замени текста с горния и натисни **„Запази и върни в опашката“**.
889
889
  3. Под **„Какво следва“** натисни **„Продължи: пусни {n} чакащи задачи в {s} сесии“** (пише
890
- **„Продължи: пусни {f} провалили се и {n} чакащи задачи в {s} сесии“**, когато във верига има
890
+ **„Продължи: пусни {f} недовършени и {n} чакащи задачи в {s} сесии“**, когато във верига има
891
891
  провалена задача).
892
892
  4. В **„Преди да продължи“** всички задачи са отметнати. За да пуснеш само поправената, натисни
893
893
  **„само тази“** до нея.
@@ -897,7 +897,7 @@ const PHASE3_BG = `
897
897
  Когато е сгрешена проверката, а не prompt-ът, се редактира друго: **„Редактирай“** на картата
898
898
  на задачата, после **„Какво проверява“**, **„Трябва“** и **„Команда“** под **„Проверки“**, после
899
899
  **„Запази и върни в опашката“** — който, като всеки бутон за връщане в опашката, сам не пуска нищо.
900
- Задача, завършила с **limit-reached**, продължава в собствения си чат: **„Продължи в същия чат“**
900
+ Задача, завършила с **limit-reached** или **aborted**, защото ботът е спрял под нея, продължава в собствения си чат: **„Продължи в същия чат“**
901
901
  на картата ѝ на страницата на сесията, после **„Пусни {n} задача(и)“** горе на същата страница.
902
902
  Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
903
903
  останалото, докато го разбереш.
@@ -1442,8 +1442,8 @@ export const SYSTEM_GUIDE = [
1442
1442
  kind: 'button',
1443
1443
  en: 'Continue in the same chat',
1444
1444
  bg: 'Продължи в същия чат',
1445
- doesEn: 'Only on a task stopped at its limit: queues it to carry on in the same chat and branch with a fresh count. Then press "Run {n} task(s)".',
1446
- doesBg: 'Само на задача, спряла на лимита: нарежда я да продължи в същия чат и клон с нов брояч. После натиснете „Пусни {n} задача(и)“.',
1445
+ doesEn: 'On a task that stopped before it finished (its limit, the bot stopping, the operator): queues it to carry on in the same chat and branch. Then "Run {n} task(s)".',
1446
+ doesBg: 'На задача, спряла преди да приключи (лимит, спрял бот, оператор): нарежда я да продължи в същия чат и клон. После „Пусни {n} задача(и)“.',
1447
1447
  },
1448
1448
  {
1449
1449
  key: 'task.rerun',
@@ -2316,6 +2316,14 @@ export const SYSTEM_GUIDE = [
2316
2316
  en: 'What is next',
2317
2317
  bg: 'Какво следва',
2318
2318
  controls: [
2319
+ {
2320
+ key: 'reg.interruptedTitle',
2321
+ kind: 'note',
2322
+ en: 'The bot stopped in the middle of the work.',
2323
+ bg: 'Ботът спря посред работата.',
2324
+ doesEn: 'After the bot stopped under a run (power, Ctrl+C, a crash): the stopped tasks\' work is kept, and "Continue" carries them on where they stopped.',
2325
+ doesBg: 'След като ботът е спрял посред пускане (ток, Ctrl+C, срив): свършеното е запазено, а „Продължи“ продължава спрелите задачи оттам, където са спрели.',
2326
+ },
2319
2327
  {
2320
2328
  key: 'batch.pause',
2321
2329
  kind: 'button',
@@ -2351,8 +2359,8 @@ export const SYSTEM_GUIDE = [
2351
2359
  {
2352
2360
  key: 'reg.continueWithFailed',
2353
2361
  kind: 'button',
2354
- en: 'Continue: run {f} failed and {n} queued task(s) in {s} session(s)',
2355
- bg: 'Продължи: пусни {f} провалили се и {n} чакащи задачи в {s} сесии',
2362
+ en: 'Continue: run {f} unfinished and {n} queued task(s) in {s} session(s)',
2363
+ bg: 'Продължи: пусни {f} недовършени и {n} чакащи задачи в {s} сесии',
2356
2364
  doesEn: 'The same button when a failed task of a chained session would go back into the queue with the queued ones.',
2357
2365
  doesBg: 'Същият бутон, когато провалена задача от сесия-верига би се върнала в опашката заедно с чакащите.',
2358
2366
  },
@@ -2417,8 +2425,8 @@ export const SYSTEM_GUIDE = [
2417
2425
  kind: 'button',
2418
2426
  en: 'Continue without asking',
2419
2427
  bg: 'Продължи без да пита',
2420
- doesEn: 'Starts exactly the ticked tasks and runs every command without asking (asks "are you sure" first unless Settings say otherwise). Ticked failed tasks start afresh.',
2421
- doesBg: 'Пуска точно отметнатите задачи и изпълнява всяка команда без питане (първо пита „сигурен ли си“, освен ако Настройките казват друго). Отметнатите провалени задачи тръгват отначало.',
2428
+ doesEn: 'Starts exactly the ticked tasks without asking (after "are you sure" unless Settings say otherwise). Stopped ones (aborted, limit reached) continue where they stopped; failed or blocked ones start afresh.',
2429
+ doesBg: 'Пуска точно отметнатите задачи без питане (след „сигурен ли си“, освен ако Настройките казват друго). Спрелите (прекратени, с лимит) продължават откъдето спряха; провалените и блокираните тръгват отначало.',
2422
2430
  },
2423
2431
  {
2424
2432
  key: 'reg.continueGo',
@@ -2789,6 +2797,14 @@ export const SYSTEM_GUIDE = [
2789
2797
  doesEn: 'How many minutes one task may run (default 240) before it stops as limit reached; saved as you type.',
2790
2798
  doesBg: 'Колко минути може да върви една задача (по подразбиране 240), преди да спре като достигнат лимит; запазва се, докато пишете.',
2791
2799
  },
2800
+ {
2801
+ key: 'exec.replyTimeout',
2802
+ kind: 'field',
2803
+ en: 'How long to wait for a reply from the chat',
2804
+ bg: 'Колко да се чака отговор от чата',
2805
+ doesEn: 'Seconds to wait for a chat reply (default 900); a reply not finished by then fails the task with "Copilot did not finish a reply within …".',
2806
+ doesBg: 'Секунди чакане на отговор от чата (по подразбиране 900); незавършен дотогава отговор проваля задачата с „Copilot did not finish a reply within …“.',
2807
+ },
2792
2808
  {
2793
2809
  key: 'exec.retryBlocked',
2794
2810
  kind: 'field',
@@ -1,17 +1,4 @@
1
- /**
2
- * Builds the messages that open a task in the chat.
3
- *
4
- * Two shapes, because a conversation is opened once and then reused:
5
- *
6
- * first task in a session level 1 (the contract, sent on its own and acknowledged),
7
- * then level 2 plus the task in one message
8
- * later tasks a short reminder that the contract still applies,
9
- * then level 2 plus the task
10
- *
11
- * Level 2 is always sent with the task, even when it did not change, because it is cheap
12
- * and it keeps each task self-contained in the transcript. The priority of level 1 over
13
- * level 2 is stated inside level 1 itself; the composition only makes the boundary visible.
14
- */
1
+ import { describeInterruption } from './interruption.js';
15
2
  const LEVEL2_HEADER = '## Project instructions (level 2)';
16
3
  const TASK_HEADER = '## Task';
17
4
  /**
@@ -50,6 +37,25 @@ function level2Block(level2) {
50
37
  * The messages to send, in order. The last one is the one whose reply starts the loop.
51
38
  * `firstMessage` is what the UI shows as "the prompt that opened this task".
52
39
  */
40
+ /**
41
+ * Why the attempt being continued stopped, and where it was, in the words the chat is sent. The
42
+ * chat was mid-conversation when it stopped, so what it most needs is to know that nothing it did
43
+ * failed, that the files are as its work left them, and — after the bot stopped under it — which of
44
+ * the steps it had just asked for actually ran.
45
+ */
46
+ function whyItStopped(c) {
47
+ if (c.how === 'interrupted') {
48
+ const where = c.interruption ? `\n\n${describeInterruption(c.interruption)}\n\n` : ' ';
49
+ return ('The runner stopped unexpectedly while this task was in progress — the program was closed, the machine went off or it ' +
50
+ 'crashed; nothing you did failed — and it has now started again. The files are as your work left them, and what had ' +
51
+ `changed is committed on this branch.${where}`.trimEnd());
52
+ }
53
+ if (c.how === 'stopped') {
54
+ return 'It was stopped by the operator before it finished — not because anything failed. The files are as you left them.';
55
+ }
56
+ const why = c.stoppedBecause ? ` (${c.stoppedBecause})` : '';
57
+ return `It stopped because the runner's limit was reached${why} — not because anything failed. The files are as you left them.`;
58
+ }
53
59
  export function composeOpening(input) {
54
60
  const taskBlock = `${TASK_HEADER} ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}\n\n${input.prompt.trim()}`;
55
61
  /*
@@ -58,18 +64,17 @@ export function composeOpening(input) {
58
64
  * conversation was lost does the task go out in full, with a line saying it is a continuation.
59
65
  */
60
66
  if (input.continuing && input.contractAlreadySent) {
61
- const why = input.continuing.stoppedBecause ? ` (${input.continuing.stoppedBecause})` : '';
62
67
  const message = `Continue task ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}, in this same conversation. ` +
63
- `It stopped because the runner's limit was reached${why} — not because anything failed. ` +
64
- `The files are as you left them. Do not start over and do not repeat work that is done: start from ` +
68
+ `${whyItStopped(input.continuing)} ` +
69
+ `Do not start over and do not repeat work that is done: start from ` +
65
70
  `your plan, say in \`notes\` which stages are done and which one you are on, and carry on. ` +
66
71
  `The assignment is the one you were given above; it has not changed. Step numbering restarts at 1.\n\n` +
67
72
  `${runnerBlock(input)}`.trimEnd();
68
73
  return { messages: [message], firstMessage: message };
69
74
  }
70
75
  const continuationLine = input.continuing
71
- ? `\n\nThis continues an earlier attempt at this task that stopped at the runner's limit. The files are as ` +
72
- `that attempt left them: look at what is already done before changing anything, and carry on from there.`
76
+ ? `\n\nThis continues an earlier attempt at this task. ${whyItStopped(input.continuing)} ` +
77
+ `Look at what is already done before changing anything, and carry on from there.`
73
78
  : input.buildsOn
74
79
  ? `\n\nThis task was done once already (attempt ${input.buildsOn.fromAttempt}), and that work is in your ` +
75
80
  `working tree. The text above is a new instruction for it: look at what is there first, build on it, and ` +
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Where an attempt was when the bot stopped under it — the power went, the machine shut down,
3
+ * `npm start` was stopped with Ctrl+C, the process crashed — read back from what it had written.
4
+ *
5
+ * Nothing in memory survives that, but the run folder does, and it was written as the work went:
6
+ * the transcript has every reply and every step as it was proposed, started and finished, and each
7
+ * step's output is in its own log under `steps/`. So the last reply the chat sent, which of the
8
+ * steps it asked for had finished (and how), which one was cut off while it ran, which never ran,
9
+ * and whether their results had already gone back to the chat — all of that is on disk. It is what
10
+ * the chat needs to be told to carry on rather than start over, and what the operator needs to see
11
+ * to trust that it can.
12
+ */
13
+ import { readFile } from 'node:fs/promises';
14
+ import { join } from 'node:path';
15
+ import { parseStepLog } from './story.js';
16
+ const TAIL_CHARS = 1200;
17
+ /** Reads an attempt's run folder; null when there is no transcript to read. */
18
+ export async function readInterruption(runDir) {
19
+ const raw = await readFile(join(runDir, 'transcript.jsonl'), 'utf8').catch(() => null);
20
+ if (raw === null)
21
+ return null;
22
+ const events = [];
23
+ for (const line of raw.split('\n')) {
24
+ if (!line.trim())
25
+ continue;
26
+ try {
27
+ events.push(JSON.parse(line));
28
+ }
29
+ catch {
30
+ // A line cut in half by the stop itself: the rest is still good.
31
+ }
32
+ }
33
+ const lastActivity = events.at(-1)?.at;
34
+ let replyAt = -1;
35
+ for (let i = events.length - 1; i >= 0; i -= 1) {
36
+ if (events[i].type === 'reply-parsed') {
37
+ replyAt = i;
38
+ break;
39
+ }
40
+ }
41
+ if (replyAt < 0)
42
+ return { lastActivity, iteration: 0, steps: [], resultsSent: false };
43
+ const reply = events[replyAt];
44
+ const iteration = Number(reply.iteration) || 0;
45
+ const after = events.slice(replyAt + 1);
46
+ const count = Number(reply.steps) || 0;
47
+ const steps = [];
48
+ for (let id = 1; id <= count; id += 1) {
49
+ const finished = after.find((e) => e.type === 'step-finished' && e.id === id);
50
+ const skipped = after.find((e) => e.type === 'step-skipped' && e.id === id);
51
+ const started = after.some((e) => e.type === 'step-started' && e.id === id);
52
+ const proposed = after.find((e) => e.type === 'step-proposed' && e.id === id);
53
+ const state = finished ? 'finished' : skipped ? 'skipped' : started ? 'cut' : 'not-run';
54
+ const step = { id, state };
55
+ if (typeof proposed?.description === 'string')
56
+ step.command = proposed.description;
57
+ if (finished) {
58
+ step.outcome = String(finished.outcome ?? '');
59
+ if (typeof finished.exitCode === 'number')
60
+ step.exitCode = finished.exitCode;
61
+ }
62
+ if (state === 'finished' || state === 'cut') {
63
+ const log = await readFile(join(runDir, 'steps', `${iteration}-${id}.log`), 'utf8').catch(() => '');
64
+ const output = parseStepLog(log).output;
65
+ if (output)
66
+ step.outputTail = output.length > TAIL_CHARS ? `…${output.slice(-TAIL_CHARS)}` : output;
67
+ }
68
+ steps.push(step);
69
+ }
70
+ // Sent back only when the results were written *and* a message went out after them.
71
+ const reportAt = after.findIndex((e) => e.type === 'report-written' && Number(e.iteration) === iteration);
72
+ const resultsSent = reportAt >= 0 && after.slice(reportAt + 1).some((e) => e.type === 'message-sent');
73
+ return { lastActivity, iteration, replyStatus: typeof reply.status === 'string' ? reply.status : undefined, steps, resultsSent };
74
+ }
75
+ /**
76
+ * What the chat is told about it, in the continuation message: which steps ran, with the end of
77
+ * their output, which was cut off, which never ran — or that the results were already sent.
78
+ */
79
+ export function describeInterruption(i) {
80
+ if (i.iteration === 0)
81
+ return 'It stopped before your first reply arrived, so nothing of this task had been run yet.';
82
+ if (i.resultsSent) {
83
+ return `The results of the steps from your last reply were already sent to you (above), and your answer to them was not received. Carry on from those results.`;
84
+ }
85
+ if (i.replyStatus === 'done' && i.steps.length === 0) {
86
+ return 'Your last reply reported the task done; the runner was checking it when it stopped. If nothing is left, reply again with your summary.';
87
+ }
88
+ if (i.steps.length === 0)
89
+ return 'Your last reply asked for no steps.';
90
+ const lines = [`Of the ${i.steps.length} step(s) in your last reply:`];
91
+ for (const s of i.steps) {
92
+ const what = s.state === 'finished'
93
+ ? `ran to the end (exit ${s.exitCode ?? '?'})`
94
+ : s.state === 'cut'
95
+ ? 'was running when the runner stopped — it may have done only part of its work; check its effect before relying on it'
96
+ : s.state === 'skipped'
97
+ ? 'was not run (refused or skipped)'
98
+ : 'did not run';
99
+ lines.push(`- step ${s.id} ${what}.`);
100
+ if (s.outputTail)
101
+ lines.push(' Its output ended with:', ' ```', ...s.outputTail.split('\n').map((l) => ` ${l}`), ' ```');
102
+ }
103
+ return lines.join('\n');
104
+ }
@@ -292,18 +292,30 @@ export class SessionStore {
292
292
  * the old attempt is untouched too, because the next attempt gets a folder of its own.
293
293
  */
294
294
  /**
295
- * Queues a task that stopped at the runner's limit to carry on where it stopped: the same
295
+ * Queues a task that stopped before it finished to carry on where it stopped: the same
296
296
  * conversation, the same branch, a fresh count of messages. The attempt that stopped is kept on
297
297
  * the record like any other.
298
+ *
299
+ * Two kinds of stop qualify. The runner's limit (`limit-reached`), and a stop that was not the
300
+ * task's doing (`aborted`): the bot itself stopping under it — power, a shutdown, Ctrl+C, a crash,
301
+ * recorded in `interruption` at the next start — or the operator stopping it. A task that failed,
302
+ * blocked or was not done has a verdict on its work, and carrying that work on would carry the
303
+ * verdict's cause on with it; those are run again.
298
304
  */
299
305
  async continueTask(sessionId, taskId) {
300
306
  const current = (await this.getSession(sessionId))?.tasks.find((x) => x.id === taskId);
301
307
  if (!current)
302
308
  throw new Error(`Task ${taskId} does not exist in session ${sessionId}.`);
303
- if (current.status !== 'limit-reached') {
304
- throw new Error('Only a task that stopped at the runner\'s limit can be continued; run this one again instead.');
309
+ if (current.status !== 'limit-reached' && current.status !== 'aborted') {
310
+ throw new Error('Only a task that stopped before it finished can be continued; run this one again instead.');
305
311
  }
306
- return await this.rerunTask(sessionId, taskId, {}, { fromAttempt: current.attempt ?? 1, stoppedBecause: current.reason });
312
+ const how = current.status === 'limit-reached' ? 'limit' : current.interruption ? 'interrupted' : 'stopped';
313
+ return await this.rerunTask(sessionId, taskId, {}, {
314
+ fromAttempt: current.attempt ?? 1,
315
+ stoppedBecause: current.reason,
316
+ how,
317
+ ...(current.interruption ? { interruption: current.interruption } : {}),
318
+ });
307
319
  }
308
320
  async rerunTask(sessionId, taskId, patch = {},
309
321
  /** Present when the new attempt carries on from the last one (see `continueTask`). */
@@ -359,6 +371,8 @@ export class SessionStore {
359
371
  t.iterations = 0;
360
372
  t.continuing = continuing;
361
373
  t.buildsOn = buildsOn;
374
+ // Where the attempt that stopped had got to belongs to that attempt; the next one starts clean.
375
+ t.interruption = undefined;
362
376
  // Cleared so the next run starts from nothing and gets its own run folder.
363
377
  t.runId = undefined;
364
378
  t.runGroup = undefined;
@@ -407,7 +421,9 @@ export class SessionStore {
407
421
  for (const t of stuck) {
408
422
  t.status = 'aborted';
409
423
  t.finishedAt = t.finishedAt ?? new Date().toISOString();
410
- t.reason = 'The operator stopped while this task was in progress, so it never finished. Nothing was resumed.';
424
+ t.reason =
425
+ 'The bot stopped while this task was in progress (the program was closed, the machine went off, or it crashed), so it never finished. ' +
426
+ 'Its work so far is kept; "Continue in the same chat" carries it on where it stopped.';
411
427
  recovered.push({ sessionId: session.id, taskId: t.id, title: t.title });
412
428
  }
413
429
  session.status = 'idle';
@@ -51,7 +51,7 @@ function sectionsOf(log) {
51
51
  return out;
52
52
  }
53
53
  /** A step log: two header lines, the output, one footer line. */
54
- function parseStepLog(text) {
54
+ export function parseStepLog(text) {
55
55
  const lines = text.split('\n');
56
56
  const command = lines[1]?.replace(/^# /, '') ?? '';
57
57
  const footer = lines.map((l) => l.match(/^# outcome=(\S+) exit=(-?\d+) durationMs=(\d+)/)).filter(Boolean).pop();
package/dist/web/404.html CHANGED
@@ -1,4 +1,4 @@
1
- <!DOCTYPE html><!--DCpEMjhrg9APkRwBBw5Do--><html lang="en" data-theme="light" data-textsize="normal" data-contrast="normal" data-motion="normal" data-links="plain" data-focus="normal"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/b67acb571615432b.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js"/><script src="/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js" async=""></script><script src="/_next/static/chunks/131-bae98f790a541679.js" async=""></script><script src="/_next/static/chunks/main-app-e3a5f713ba31557e.js" async=""></script><script src="/_next/static/chunks/197-f7c33a0114fc3db3.js" async=""></script><script src="/_next/static/chunks/867-4aa80f5a32311bce.js" async=""></script><script src="/_next/static/chunks/549-33f0d3d53338b2f2.js" async=""></script><script src="/_next/static/chunks/app/layout-e0174d863960f109.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>copilot-operator</title><meta name="description" content="Runs task loops with Microsoft 365 Copilot on this machine."/><script>
1
+ <!DOCTYPE html><!--kysrkUS5Y_ZTHsIIXgs3q--><html lang="en" data-theme="light" data-textsize="normal" data-contrast="normal" data-motion="normal" data-links="plain" data-focus="normal"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/b67acb571615432b.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js"/><script src="/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js" async=""></script><script src="/_next/static/chunks/131-bae98f790a541679.js" async=""></script><script src="/_next/static/chunks/main-app-e3a5f713ba31557e.js" async=""></script><script src="/_next/static/chunks/197-f7c33a0114fc3db3.js" async=""></script><script src="/_next/static/chunks/867-3d4ee08c5f9f0e71.js" async=""></script><script src="/_next/static/chunks/549-33f0d3d53338b2f2.js" async=""></script><script src="/_next/static/chunks/app/layout-e0174d863960f109.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>copilot-operator</title><meta name="description" content="Runs task loops with Microsoft 365 Copilot on this machine."/><script>
2
2
  (function () {
3
3
  try {
4
4
  var d = document.documentElement;
@@ -12,4 +12,4 @@
12
12
  d.dataset.focus = a.strongFocus ? 'strong' : 'normal';
13
13
  } catch (e) {}
14
14
  })();
15
- </script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><a class="skip-link" href="#main">Skip to the content</a><div class="wrap"><header class="top"><h1><a href="/">copilot-operator</a></h1><nav aria-label="Main"><a href="/"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v4H4zM4 11h16v4H4zM4 17h10v2H4z"></path></svg><span>Sessions</span></a><a href="/defaults"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.5 4 1.5 1-1.6 2.8-1.8-.4a7 7 0 0 1-1.5 1.5l.4 1.8L14.7 21l-1-1.5a7 7 0 0 1-3.4 0l-1 1.5-2.8-1.6.4-1.8A7 7 0 0 1 5.4 16l-1.8.4L2 13.6 3.5 12.6V11.4L2 10.4l1.6-2.8 1.8.4A7 7 0 0 1 6.9 6.5l-.4-1.8L9.3 3l1 1.5a7 7 0 0 1 3.4 0l1-1.5 2.8 1.6-.4 1.8a7 7 0 0 1 1.5 1.5l1.8-.4L22 10.4l-1.5 1z"></path></svg><span>Settings</span></a><a href="/import"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3v10m0 0 4-4m-4 4-4-4M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Plan from JSON</span></a><a href="/history"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 1 0 8.5 6M12 7v5l3 2M3 4v5h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Task register</span></a><a href="/level1"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 13h7M9 17h7" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 1 prompt</span></a><a href="/presets"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M5 4h14v4H5zM5 10h14v4H5zM5 16h9v4H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 2 presets</span></a><a href="/appearance"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 0 0 0 18c1.5 0 2-1 2-2s-1-1.5-1-2.5S14 15 15.5 15H17a4 4 0 0 0 4-4 8 8 0 0 0-9-8zM8 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4-3a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"></path></svg><span>Appearance</span></a><a href="/system"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v10H4zM2 19h20M9 15v4M15 15v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>System</span></a><select aria-label="Language" style="width:auto"><option value="en" selected="">English</option><option value="bg">Български</option></select></nav></header><main id="main"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></main></div><script src="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-4aa80f5a32311bce.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"ScrollJump\"]\nd:I[433,[],\"OutletBoundary\"]\nf:I[9884,[],\"AsyncMeta"])</script><script>self.__next_f.push([1,"dataOutlet\"]\n11:I[433,[],\"ViewportBoundary\"]\n13:I[433,[],\"MetadataBoundary\"]\n14:\"$Sreact.suspense\"\n16:I[3536,[],\"\"]\n:HL[\"/_next/static/css/b67acb571615432b.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"DCpEMjhrg9APkRwBBw5Do\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b67acb571615432b.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"data-theme\":\"light\",\"data-textsize\":\"normal\",\"data-contrast\":\"normal\",\"data-motion\":\"normal\",\"data-links\":\"plain\",\"data-focus\":\"normal\",\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"$2\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$L5\",null,{\"children\":[[\"$\",\"$L6\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L7\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L8\",null,{}]]}],[\"$\",\"$L9\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lc\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$Ld\",null,{\"children\":[\"$Le\",[\"$\",\"$Lf\",null,{\"promise\":\"$@10\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L11\",null,{\"children\":\"$L12\"}],null],[\"$\",\"$L13\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$14\",null,{\"fallback\":null,\"children\":\"$L15\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$16\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"12:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\ne:null\n"])</script><script>self.__next_f.push([1,"10:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"copilot-operator\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Runs task loops with Microsoft 365 Copilot on this machine.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"15:\"$10:metadata\"\n"])</script></body></html>
15
+ </script><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body><div hidden=""><!--$--><!--/$--></div><a class="skip-link" href="#main">Skip to the content</a><div class="wrap"><header class="top"><h1><a href="/">copilot-operator</a></h1><nav aria-label="Main"><a href="/"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v4H4zM4 11h16v4H4zM4 17h10v2H4z"></path></svg><span>Sessions</span></a><a href="/defaults"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm8.5 4 1.5 1-1.6 2.8-1.8-.4a7 7 0 0 1-1.5 1.5l.4 1.8L14.7 21l-1-1.5a7 7 0 0 1-3.4 0l-1 1.5-2.8-1.6.4-1.8A7 7 0 0 1 5.4 16l-1.8.4L2 13.6 3.5 12.6V11.4L2 10.4l1.6-2.8 1.8.4A7 7 0 0 1 6.9 6.5l-.4-1.8L9.3 3l1 1.5a7 7 0 0 1 3.4 0l1-1.5 2.8 1.6-.4 1.8a7 7 0 0 1 1.5 1.5l1.8-.4L22 10.4l-1.5 1z"></path></svg><span>Settings</span></a><a href="/import"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3v10m0 0 4-4m-4 4-4-4M4 15v4a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Plan from JSON</span></a><a href="/history"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 1 0 8.5 6M12 7v5l3 2M3 4v5h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg><span>Task register</span></a><a href="/level1"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M6 3h9l5 5v13H6zM14 3v6h6M9 13h7M9 17h7" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 1 prompt</span></a><a href="/presets"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M5 4h14v4H5zM5 10h14v4H5zM5 16h9v4H5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>Level 2 presets</span></a><a href="/appearance"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M12 3a9 9 0 0 0 0 18c1.5 0 2-1 2-2s-1-1.5-1-2.5S14 15 15.5 15H17a4 4 0 0 0 4-4 8 8 0 0 0-9-8zM8 9a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4-3a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm4 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"></path></svg><span>Appearance</span></a><a href="/system"><svg class="nav-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true"><path d="M4 5h16v10H4zM2 19h20M9 15v4M15 15v4" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"></path></svg><span>System</span></a><select aria-label="Language" style="width:auto"><option value="en" selected="">English</option><option value="bg">Български</option></select></nav></header><main id="main"><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--></main></div><script src="/_next/static/chunks/webpack-0f31b926d9a9c5a4.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"themeScript\"]\n3:I[1205,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"AppearanceProvider\"]\n4:I[867,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"LanguageProvider\"]\n5:I[1035,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"DialogHost\"]\n6:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"SkipLink\"]\n7:I[8197,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"\"]\n8:I[8214,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"Nav\"]\n9:I[6657,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"GlobalApprovals\"]\na:I[5204,[],\"\"]\nb:I[2642,[],\"\"]\nc:I[7712,[\"197\",\"static/chunks/197-f7c33a0114fc3db3.js\",\"867\",\"static/chunks/867-3d4ee08c5f9f0e71.js\",\"549\",\"static/chunks/549-33f0d3d53338b2f2.js\",\"177\",\"static/chunks/app/layout-e0174d863960f109.js\"],\"ScrollJump\"]\nd:I[433,[],\"OutletBoundary\"]\nf:I[9884,[],\"AsyncMeta"])</script><script>self.__next_f.push([1,"dataOutlet\"]\n11:I[433,[],\"ViewportBoundary\"]\n13:I[433,[],\"MetadataBoundary\"]\n14:\"$Sreact.suspense\"\n16:I[3536,[],\"\"]\n:HL[\"/_next/static/css/b67acb571615432b.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"kysrkUS5Y-ZTHsIIXgs3q\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b67acb571615432b.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"suppressHydrationWarning\":true,\"data-theme\":\"light\",\"data-textsize\":\"normal\",\"data-contrast\":\"normal\",\"data-motion\":\"normal\",\"data-links\":\"plain\",\"data-focus\":\"normal\",\"children\":[[\"$\",\"head\",null,{\"children\":[\"$\",\"script\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"$2\"}}]}],[\"$\",\"body\",null,{\"children\":[\"$\",\"$L3\",null,{\"children\":[\"$\",\"$L4\",null,{\"children\":[\"$\",\"$L5\",null,{\"children\":[[\"$\",\"$L6\",null,{}],[\"$\",\"div\",null,{\"className\":\"wrap\",\"children\":[[\"$\",\"header\",null,{\"className\":\"top\",\"children\":[[\"$\",\"h1\",null,{\"children\":[\"$\",\"$L7\",null,{\"href\":\"/\",\"children\":\"copilot-operator\"}]}],[\"$\",\"$L8\",null,{}]]}],[\"$\",\"$L9\",null,{}],[\"$\",\"main\",null,{\"id\":\"main\",\"children\":[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]]}],[\"$\",\"$Lc\",null,{}]]}]}]}]}]]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$La\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$Lb\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$Ld\",null,{\"children\":[\"$Le\",[\"$\",\"$Lf\",null,{\"promise\":\"$@10\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L11\",null,{\"children\":\"$L12\"}],null],[\"$\",\"$L13\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$14\",null,{\"fallback\":null,\"children\":\"$L15\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$16\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"12:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\ne:null\n"])</script><script>self.__next_f.push([1,"10:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"copilot-operator\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Runs task loops with Microsoft 365 Copilot on this machine.\"}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"15:\"$10:metadata\"\n"])</script></body></html>