mandala-computer-mcp 0.3.0 → 0.5.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 (130) hide show
  1. package/README.md +462 -37
  2. package/dist/api.d.ts +12 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +262 -42
  5. package/dist/api.js.map +1 -1
  6. package/dist/artifacts.d.ts +63 -0
  7. package/dist/artifacts.d.ts.map +1 -0
  8. package/dist/artifacts.js +81 -0
  9. package/dist/artifacts.js.map +1 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +35 -21
  13. package/dist/cli.js.map +1 -1
  14. package/dist/credentials.d.ts +33 -0
  15. package/dist/credentials.d.ts.map +1 -0
  16. package/dist/credentials.js +395 -0
  17. package/dist/credentials.js.map +1 -0
  18. package/dist/errors.d.ts +53 -10
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +122 -35
  21. package/dist/errors.js.map +1 -1
  22. package/dist/events.d.ts +11 -2
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +213 -95
  25. package/dist/events.js.map +1 -1
  26. package/dist/executions.d.ts +43 -0
  27. package/dist/executions.d.ts.map +1 -0
  28. package/dist/executions.js +162 -0
  29. package/dist/executions.js.map +1 -0
  30. package/dist/format.d.ts +11 -11
  31. package/dist/format.d.ts.map +1 -1
  32. package/dist/format.js +159 -16
  33. package/dist/format.js.map +1 -1
  34. package/dist/http.d.ts.map +1 -1
  35. package/dist/http.js +4 -0
  36. package/dist/http.js.map +1 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/index.js.map +1 -1
  41. package/dist/limits.d.ts +17 -0
  42. package/dist/limits.d.ts.map +1 -0
  43. package/dist/limits.js +17 -0
  44. package/dist/limits.js.map +1 -0
  45. package/dist/paths.d.ts +42 -2
  46. package/dist/paths.d.ts.map +1 -1
  47. package/dist/paths.js +54 -3
  48. package/dist/paths.js.map +1 -1
  49. package/dist/poll.d.ts +5 -1
  50. package/dist/poll.d.ts.map +1 -1
  51. package/dist/poll.js +120 -16
  52. package/dist/poll.js.map +1 -1
  53. package/dist/results.d.ts +97 -0
  54. package/dist/results.d.ts.map +1 -0
  55. package/dist/results.js +263 -0
  56. package/dist/results.js.map +1 -0
  57. package/dist/server.d.ts +3 -2
  58. package/dist/server.d.ts.map +1 -1
  59. package/dist/server.js +61 -9
  60. package/dist/server.js.map +1 -1
  61. package/dist/stdio.d.ts +5 -1
  62. package/dist/stdio.d.ts.map +1 -1
  63. package/dist/stdio.js +10 -4
  64. package/dist/stdio.js.map +1 -1
  65. package/dist/tool-filters.d.ts +38 -0
  66. package/dist/tool-filters.d.ts.map +1 -0
  67. package/dist/tool-filters.js +129 -0
  68. package/dist/tool-filters.js.map +1 -0
  69. package/dist/tools/account.d.ts +3 -0
  70. package/dist/tools/account.d.ts.map +1 -0
  71. package/dist/tools/account.js +122 -0
  72. package/dist/tools/account.js.map +1 -0
  73. package/dist/tools/activities.d.ts +3 -0
  74. package/dist/tools/activities.d.ts.map +1 -0
  75. package/dist/tools/activities.js +140 -0
  76. package/dist/tools/activities.js.map +1 -0
  77. package/dist/tools/agent.d.ts.map +1 -1
  78. package/dist/tools/agent.js +72 -5
  79. package/dist/tools/agent.js.map +1 -1
  80. package/dist/tools/artifacts.d.ts +3 -0
  81. package/dist/tools/artifacts.d.ts.map +1 -0
  82. package/dist/tools/artifacts.js +97 -0
  83. package/dist/tools/artifacts.js.map +1 -0
  84. package/dist/tools/chat.d.ts +6 -0
  85. package/dist/tools/chat.d.ts.map +1 -0
  86. package/dist/tools/chat.js +192 -0
  87. package/dist/tools/chat.js.map +1 -0
  88. package/dist/tools/computers.d.ts.map +1 -1
  89. package/dist/tools/computers.js +300 -247
  90. package/dist/tools/computers.js.map +1 -1
  91. package/dist/tools/directory.d.ts +13 -0
  92. package/dist/tools/directory.d.ts.map +1 -0
  93. package/dist/tools/directory.js +88 -0
  94. package/dist/tools/directory.js.map +1 -0
  95. package/dist/tools/events.d.ts.map +1 -1
  96. package/dist/tools/events.js +66 -22
  97. package/dist/tools/events.js.map +1 -1
  98. package/dist/tools/executions.d.ts +3 -0
  99. package/dist/tools/executions.d.ts.map +1 -0
  100. package/dist/tools/executions.js +87 -0
  101. package/dist/tools/executions.js.map +1 -0
  102. package/dist/tools/guest.d.ts.map +1 -1
  103. package/dist/tools/guest.js +57 -17
  104. package/dist/tools/guest.js.map +1 -1
  105. package/dist/tools/input.d.ts.map +1 -1
  106. package/dist/tools/input.js +63 -5
  107. package/dist/tools/input.js.map +1 -1
  108. package/dist/tools/results.d.ts +30 -0
  109. package/dist/tools/results.d.ts.map +1 -0
  110. package/dist/tools/results.js +106 -0
  111. package/dist/tools/results.js.map +1 -0
  112. package/dist/tools/secrets.d.ts +58 -0
  113. package/dist/tools/secrets.d.ts.map +1 -0
  114. package/dist/tools/secrets.js +204 -0
  115. package/dist/tools/secrets.js.map +1 -0
  116. package/dist/tools/signals.d.ts +3 -0
  117. package/dist/tools/signals.d.ts.map +1 -0
  118. package/dist/tools/signals.js +116 -0
  119. package/dist/tools/signals.js.map +1 -0
  120. package/dist/tools/snapshots.d.ts.map +1 -1
  121. package/dist/tools/snapshots.js +213 -167
  122. package/dist/tools/snapshots.js.map +1 -1
  123. package/dist/tools/ssh.d.ts +3 -0
  124. package/dist/tools/ssh.d.ts.map +1 -0
  125. package/dist/tools/ssh.js +186 -0
  126. package/dist/tools/ssh.js.map +1 -0
  127. package/dist/tools/templates.d.ts.map +1 -1
  128. package/dist/tools/templates.js +9 -13
  129. package/dist/tools/templates.js.map +1 -1
  130. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ import { z } from 'zod';
2
+ import { said } from '../format.js';
3
+ import * as P from '../paths.js';
4
+ import { count, executionId, exitCode, label, metadata, metadataCall } from './directory.js';
5
+ import { computerSchema, readAnnotations } from './results.js';
6
+ const position = {
7
+ seq: count,
8
+ cursor: label,
9
+ at: label,
10
+ computer: label,
11
+ source: z.literal('daemon'),
12
+ };
13
+ const status = z.enum([
14
+ 'running',
15
+ 'stopped',
16
+ 'suspended',
17
+ 'building',
18
+ 'build-failed',
19
+ 'half-removed',
20
+ ]);
21
+ const processData = z.union([
22
+ z.object({
23
+ pid: count.min(1).max(2147483647),
24
+ execution_id: executionId.optional(),
25
+ exit_code: exitCode,
26
+ lost: z.never().optional(),
27
+ }),
28
+ z.object({
29
+ pid: count.min(1).max(2147483647),
30
+ execution_id: executionId.optional(),
31
+ lost: z.literal(true),
32
+ exit_code: z.never().optional(),
33
+ }),
34
+ ]);
35
+ const event = z.discriminatedUnion('type', [
36
+ z.object({ ...position, type: z.literal('process.exited'), data: processData }),
37
+ z.object({
38
+ ...position,
39
+ type: z.literal('computer.started'),
40
+ data: z.object({ status: z.literal('running'), previous: status.optional() }),
41
+ }),
42
+ z.object({
43
+ ...position,
44
+ type: z.literal('computer.stopped'),
45
+ data: z.object({ status: z.literal('stopped'), previous: status.optional() }),
46
+ }),
47
+ z.object({
48
+ ...position,
49
+ type: z.literal('computer.suspended'),
50
+ data: z.object({ status: z.literal('suspended'), previous: status.optional() }),
51
+ }),
52
+ z.object({
53
+ ...position,
54
+ type: z.literal('computer.idle'),
55
+ data: z.object({ idle_seconds: count }),
56
+ }),
57
+ ]);
58
+ const page = z.object({
59
+ computer: label,
60
+ from: z.string(),
61
+ cursor: label,
62
+ events: z.array(event).max(100),
63
+ more: z.boolean(),
64
+ baseline: z.boolean(),
65
+ gap: z
66
+ .object({
67
+ cursor: label,
68
+ at: label,
69
+ type: z.literal('gap'),
70
+ computer: label,
71
+ source: z.literal('daemon'),
72
+ data: z.object({ oldest_cursor: label.optional(), detail: label }),
73
+ })
74
+ .optional(),
75
+ supported: z.array(label),
76
+ retention: z.literal('ephemeral'),
77
+ });
78
+ export const registerSignals = (server, session) => {
79
+ server.registerTool('read_signals', {
80
+ title: 'Read one passive platform signal page',
81
+ description: 'Read finite daemon facts without waking the computer, guest contact, event socket, watcher, capture or polling loop. Omit since or send empty for a head-only baseline with no replay; neither a baseline nor gap proves no earlier activity or task success. Keep the returned cursor even on an empty page: filtered-out rows can advance it. Retention is ephemeral, and resets are explicit gaps.',
82
+ inputSchema: {
83
+ computer_id: computerSchema,
84
+ since: z.string().max(2048).optional(),
85
+ limit: z.number().int().min(1).max(100).optional(),
86
+ },
87
+ annotations: readAnnotations,
88
+ }, ({ computer_id, since, limit }, extra) => metadataCall(async () => {
89
+ const id = session.resolve(computer_id);
90
+ const data = metadata(page, await session.api.json('GET', P.signals(id), {
91
+ query: { since, limit },
92
+ signal: extra.signal,
93
+ }));
94
+ // Compare opaque checkpoints without parsing or deriving a next cursor.
95
+ // Baselines and explicit resets describe one head; replay starts at since.
96
+ // Events must advance; the next cursor may also pass filtered rows.
97
+ const checkpoints = new Set([data.from, ...data.events.map((e) => e.cursor)]);
98
+ if (data.computer !== id ||
99
+ data.events.some((e, i) => e.computer !== id || (i > 0 && e.seq <= data.events[i - 1].seq)) ||
100
+ checkpoints.size !== data.events.length + 1 ||
101
+ (data.events.length > 0 &&
102
+ data.cursor !== data.events.at(-1)?.cursor &&
103
+ checkpoints.has(data.cursor)) ||
104
+ data.baseline !== (since === undefined || since === '') ||
105
+ (data.baseline && data.gap) ||
106
+ (data.gap && (data.gap.computer !== id || data.gap.cursor !== data.cursor)) ||
107
+ ((data.baseline || data.gap) &&
108
+ (data.events.length || data.more || data.from !== data.cursor)) ||
109
+ (!data.baseline && !data.gap && data.from !== since) ||
110
+ (data.more && !data.events.length) ||
111
+ data.events.length > (limit ?? 50))
112
+ throw new Error('Inconsistent signal page; no checkpoint was established.');
113
+ return said(`${data.gap ? 'GAP: the checkpoint was reset. ' : ''}${data.baseline ? 'Head-only baseline; no historical replay. ' : ''}One ephemeral page; use the returned cursor, including after an empty page. These facts do not establish task success.`, data);
114
+ }));
115
+ };
116
+ //# sourceMappingURL=signals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.js","sourceRoot":"","sources":["../../src/tools/signals.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../../src/tools/snapshots.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAgM5C,eAAO,MAAM,iBAAiB,EAAE,SA6wB/B,CAAC"}
1
+ {"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../../src/tools/snapshots.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AA4M5C,eAAO,MAAM,iBAAiB,EAAE,SA40B/B,CAAC"}
@@ -123,21 +123,17 @@ const snapshotTurn = async (api, sid, unfinished) => {
123
123
  }
124
124
  return { kind: 'row', row };
125
125
  };
126
- /**
127
- * The sentence in front of a retention window, for the reason every tool here
128
- * leads with one: the model reads the text, and three integers in a JSON blob
129
- * do not say what they select.
130
- *
131
- * Says what SURVIVES rather than restating the numbers. A tier at zero is left
132
- * out entirely rather than printed as "0 monthly", which reads like a promise
133
- * about monthlies; and an all-zero window — what an account with no active
134
- * subscription reads — is stated as the plan granting no retained history,
135
- * without claiming anything about what happens to snapshots already taken. The
136
- * platform's own reference stops in the same place and for the same reason.
137
- */
138
- const retentionLine = (r) => {
139
- const v = (r ?? {});
140
- const n = (x) => (typeof x === 'number' && Number.isFinite(x) && x > 0 ? x : 0);
126
+ const retentionOf = (r) => {
127
+ if (!isRow(r))
128
+ return undefined;
129
+ const fields = [r.daily, r.weekly, r.monthly];
130
+ if (!fields.every((value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)) {
131
+ return undefined;
132
+ }
133
+ return { daily: r.daily, weekly: r.weekly, monthly: r.monthly };
134
+ };
135
+ const retentionLine = (v) => {
136
+ const n = (x) => (x > 0 ? x : 0);
141
137
  const parts = [
142
138
  n(v.daily) && `${n(v.daily)} daily`,
143
139
  n(v.weekly) && `${n(v.weekly)} weekly`,
@@ -295,7 +291,7 @@ export const registerSnapshots = (server, session, opts) => {
295
291
  memory: z
296
292
  .boolean()
297
293
  .default(false)
298
- .describe('Include the running session. A memory snapshot is a saved machine, so it only loads back into the shape it came off: resize the computer afterwards and the restore is refused, because the vCPU count and the memory size are part of the state rather than decoration around it. Clone it instead in that case, which restores the disk and boots fresh.'),
294
+ .describe('Include the running session. A memory snapshot is a saved machine, so it only loads back into the shape it came off: resize the computer afterwards and the restore is refused, because the vCPU count and the memory size are part of the state rather than decoration around it. Clone it with memory: false instead in that case, which restores the disk and boots fresh.'),
299
295
  wait: z
300
296
  .boolean()
301
297
  .default(true)
@@ -419,93 +415,98 @@ export const registerSnapshots = (server, session, opts) => {
419
415
  // single short request and there is nothing to report until there is a
420
416
  // capture to report on.
421
417
  const beat = heartbeat(extra, server.server);
422
- await beat(`Capturing ${sid} of ${id} — the copy has started.`);
423
- let blocked;
424
- // `untilDeadline &&` rather than `!untilDeadline?.aborted`: the early
425
- // return above is what guarantees it is armed here, and the optional
426
- // form would turn a later edit that broke that guarantee into a loop
427
- // with no deadline at all rather than into a visible mistake.
428
- while (untilDeadline && !untilDeadline.aborted) {
429
- // The caller giving up ends the wait. The signal aborts the request in
430
- // flight, but nothing about an aborted request stops the next
431
- // iteration from starting one.
432
- if (extra.signal?.aborted) {
433
- return refused(`Cancelled while waiting for the capture of ${id}. THE CAPTURE IS STILL RUNNING — nothing ` +
434
- `was stopped, because a disk copy already under way cannot be called back. ${byHand}`, started);
435
- }
436
- const turn = await snapshotTurn(api, sid, false);
437
- if (extra.signal?.aborted)
438
- continue;
439
- // A body stream can fail without either signal firing, so the
440
- // deadline's own arrival is what tells a real timeout from an undici
441
- // idle abort. Same shape as the two loops in computers.ts.
442
- if (turn.kind === 'cancelled') {
443
- if (untilDeadline?.aborted)
444
- break;
445
- blocked = turn.why;
446
- await beat(`Capturing ${sid} of ${id} — the platform could not be asked: ${turn.why}`);
447
- await sleep(POLL_MS, signal);
448
- continue;
449
- }
450
- // A poll that failed for a reason worth riding out is weather; one
451
- // that failed for any other reason is a real failure with a capture
452
- // still running behind it, and a thrown error's handler has no way to
453
- // say so. So it is said here rather than rethrown.
454
- if (turn.kind === 'broken') {
455
- return refused(`${turn.why}\n\nTHE CAPTURE IS STILL RUNNING — this was the poll failing, not the ` +
456
- `capture. ${byHand}`, started);
457
- }
458
- if (turn.kind === 'blocked') {
459
- blocked = turn.why;
460
- await beat(`Capturing ${sid} of ${id} — the platform could not be asked: ${turn.why}`);
461
- await sleep(turn.after ?? POLL_MS, signal);
462
- continue;
463
- }
464
- // Absence is the ONLY signal a failed capture leaves, which is what
465
- // makes snapshotTurn's guards load-bearing rather than tidy: a body
466
- // that is not a list, and a listing the platform had to answer short,
467
- // both come back `blocked` there rather than as this, because read as
468
- // "the row is not there" they would announce that somebody's backup
469
- // failed while it was still being taken.
470
- if (turn.kind === 'absent') {
471
- return refused(`THE CAPTURE OF ${id} FAILED and nothing was saved. Snapshot ${sid} is no longer listed and ` +
472
- `nothing took its place, which is what a capture that starts and then fails leaves behind — ` +
473
- `this is a failure during the copy, not a wait that ran out. The computer itself is ` +
474
- `untouched and can be snapshotted again.`, started);
475
- }
476
- const row = turn.row;
477
- const state = typeof row.state === 'string' ? row.state : undefined;
478
- if (state === CAPTURING) {
479
- blocked = undefined;
480
- await beat(`Capturing ${sid} of ${id} — still copying.`);
481
- await sleep(POLL_MS, signal);
482
- continue;
483
- }
484
- // A row with no readable `state` is the platform failing to describe
485
- // it, and it must not be read as "not capturing, therefore landed" —
486
- // that sentence says a placeholder may be restored, which is the
487
- // defect this whole tool is here to stop. Only a state that SAYS it is
488
- // no longer capturing ends the wait; anything else rides out, and the
489
- // deadline reports that the platform could not be asked.
490
- if (state === undefined) {
491
- blocked = `the row for ${sid} carried no state, so nothing said whether the capture had landed`;
492
- await beat(`Capturing ${sid} of ${id} — its row carried no state to read.`);
493
- await sleep(POLL_MS, signal);
494
- continue;
418
+ try {
419
+ await beat(`Capturing ${sid} of ${id} — the copy has started.`);
420
+ let blocked;
421
+ // `untilDeadline &&` rather than `!untilDeadline?.aborted`: the early
422
+ // return above is what guarantees it is armed here, and the optional
423
+ // form would turn a later edit that broke that guarantee into a loop
424
+ // with no deadline at all rather than into a visible mistake.
425
+ while (untilDeadline && !untilDeadline.aborted) {
426
+ // The caller giving up ends the wait. The signal aborts the request in
427
+ // flight, but nothing about an aborted request stops the next
428
+ // iteration from starting one.
429
+ if (extra.signal?.aborted) {
430
+ return refused(`Cancelled while waiting for the capture of ${id}. THE CAPTURE IS STILL RUNNING — nothing ` +
431
+ `was stopped, because a disk copy already under way cannot be called back. ${byHand}`, started);
432
+ }
433
+ const turn = await snapshotTurn(api, sid, false);
434
+ if (extra.signal?.aborted)
435
+ continue;
436
+ // A body stream can fail without either signal firing, so the
437
+ // deadline's own arrival is what tells a real timeout from an undici
438
+ // idle abort. Same shape as the two loops in computers.ts.
439
+ if (turn.kind === 'cancelled') {
440
+ if (untilDeadline?.aborted)
441
+ break;
442
+ blocked = turn.why;
443
+ await beat(`Capturing ${sid} of ${id} — the platform could not be asked: ${turn.why}`);
444
+ await sleep(POLL_MS, signal);
445
+ continue;
446
+ }
447
+ // A poll that failed for a reason worth riding out is weather; one
448
+ // that failed for any other reason is a real failure with a capture
449
+ // still running behind it, and a thrown error's handler has no way to
450
+ // say so. So it is said here rather than rethrown.
451
+ if (turn.kind === 'broken') {
452
+ return refused(`${turn.why}\n\nTHE CAPTURE IS STILL RUNNING — this was the poll failing, not the ` +
453
+ `capture. ${byHand}`, started);
454
+ }
455
+ if (turn.kind === 'blocked') {
456
+ blocked = turn.why;
457
+ await beat(`Capturing ${sid} of ${id} — the platform could not be asked: ${turn.why}`);
458
+ await sleep(turn.after ?? POLL_MS, signal);
459
+ continue;
460
+ }
461
+ // Absence is the ONLY signal a failed capture leaves, which is what
462
+ // makes snapshotTurn's guards load-bearing rather than tidy: a body
463
+ // that is not a list, and a listing the platform had to answer short,
464
+ // both come back `blocked` there rather than as this, because read as
465
+ // "the row is not there" they would announce that somebody's backup
466
+ // failed while it was still being taken.
467
+ if (turn.kind === 'absent') {
468
+ return refused(`THE CAPTURE OF ${id} FAILED and nothing was saved. Snapshot ${sid} is no longer listed and ` +
469
+ `nothing took its place, which is what a capture that starts and then fails leaves behind — ` +
470
+ `this is a failure during the copy, not a wait that ran out. The computer itself is ` +
471
+ `untouched and can be snapshotted again.`, started);
472
+ }
473
+ const row = turn.row;
474
+ const state = typeof row.state === 'string' ? row.state : undefined;
475
+ if (state === CAPTURING) {
476
+ blocked = undefined;
477
+ await beat(`Capturing ${sid} of ${id} — still copying.`);
478
+ await sleep(POLL_MS, signal);
479
+ continue;
480
+ }
481
+ // A row with no readable `state` is the platform failing to describe
482
+ // it, and it must not be read as "not capturing, therefore landed" —
483
+ // that sentence says a placeholder may be restored, which is the
484
+ // defect this whole tool is here to stop. Only a state that SAYS it is
485
+ // no longer capturing ends the wait; anything else rides out, and the
486
+ // deadline reports that the platform could not be asked.
487
+ if (state === undefined) {
488
+ blocked = `the row for ${sid} carried no state, so nothing said whether the capture had landed`;
489
+ await beat(`Capturing ${sid} of ${id} — its row carried no state to read.`);
490
+ await sleep(POLL_MS, signal);
491
+ continue;
492
+ }
493
+ return said(`${took} The capture landed: snapshot ${sid} reads "${state}" and can now be restored, ` +
494
+ `cloned or deleted.`, row);
495
495
  }
496
- return said(`${took} The capture landed: snapshot ${sid} reads "${state}" and can now be restored, ` +
497
- `cloned or deleted.`, row);
496
+ // A refusal, for the reason move_computer's is: the wait never reached
497
+ // what it was told to wait for. What it must not do is read as a capture
498
+ // that failed — that is a different answer with a different id in it,
499
+ // and the difference between calling this again and going looking for a
500
+ // snapshot that is on its way.
501
+ return refused(blocked
502
+ ? `${startedLine} Gave up watching after ${timeout_s}s; the platform could not be asked — the ` +
503
+ `last attempt said: ${blocked}. THE CAPTURE IS STILL RUNNING. ${byHand}`
504
+ : `${startedLine} Still capturing after ${timeout_s}s, which a large disk takes. THE CAPTURE IS ` +
505
+ `STILL RUNNING and nothing was changed by giving up on the wait. ${byHand}`, started);
506
+ }
507
+ finally {
508
+ await beat.stop();
498
509
  }
499
- // A refusal, for the reason move_computer's is: the wait never reached
500
- // what it was told to wait for. What it must not do is read as a capture
501
- // that failed — that is a different answer with a different id in it,
502
- // and the difference between calling this again and going looking for a
503
- // snapshot that is on its way.
504
- return refused(blocked
505
- ? `${startedLine} Gave up watching after ${timeout_s}s; the platform could not be asked — the ` +
506
- `last attempt said: ${blocked}. THE CAPTURE IS STILL RUNNING. ${byHand}`
507
- : `${startedLine} Still capturing after ${timeout_s}s, which a large disk takes. THE CAPTURE IS ` +
508
- `STILL RUNNING and nothing was changed by giving up on the wait. ${byHand}`, started);
509
510
  }));
510
511
  server.registerTool('restore_snapshot', {
511
512
  title: 'Restore a snapshot',
@@ -580,7 +581,11 @@ export const registerSnapshots = (server, session, opts) => {
580
581
  annotations: { readOnlyHint: true },
581
582
  }, (_args, extra) => guarded(async () => {
582
583
  const body = await session.api.with(extra.signal).json('GET', P.RETENTION);
583
- return said(retentionLine(body), body);
584
+ const retention = retentionOf(body);
585
+ if (!retention) {
586
+ return refused('The retention policy could not be read: GET /retention must answer with nonnegative whole-number `daily`, `weekly`, and `monthly` fields. No retention window is inferred from this response.', body);
587
+ }
588
+ return said(retentionLine(retention), body);
584
589
  }));
585
590
  // clone_snapshot brings a billable machine into existence as surely as
586
591
  // create_computer does, and delete_snapshot destroys bytes as surely as
@@ -594,27 +599,63 @@ export const registerSnapshots = (server, session, opts) => {
594
599
  return;
595
600
  server.registerTool('clone_snapshot', {
596
601
  title: 'Fork a snapshot into a new computer',
597
- description: 'Build a new computer from a snapshot, leaving the original untouched. This is the fork half of snapshot-and-fork, and the only thing that works on an orphaned snapshot.',
602
+ description: "Build a new computer from a snapshot, leaving the original untouched. This is the fork half of snapshot-and-fork, and the only thing that works on an orphaned snapshot. A MEMORY snapshot is resumed: the copy comes up running the captured session, sharing its source's network identity, so it cannot run on the same host while its source is running. Pass memory: false to build it from the disk alone instead, as a fresh boot with its own identity. A memory snapshot of a computer that held SECRETS is resumed only with inherit_secrets: true, and then the copy HOLDS THE SAME CREDENTIALS as the computer it came from; without it the clone is built from the disk and this tool says so.",
598
603
  inputSchema: {
599
604
  snapshot_id: z.string(),
600
605
  name: z.string().optional().describe('A name for the new computer.'),
606
+ memory: z
607
+ .boolean()
608
+ .optional()
609
+ .describe('Memory snapshots only. false builds the new computer from the disk alone and boots it fresh; omitted or true resumes the captured session.'),
610
+ inherit_secrets: z
611
+ .boolean()
612
+ .optional()
613
+ .describe('Consent to resuming a memory snapshot of a computer that held secrets. The new computer holds the same credentials, bound to the same secrets. Only set this when that is what the user asked for.'),
601
614
  select: z
602
615
  .boolean()
603
616
  .default(true)
604
617
  .describe("Make the new computer this session's selected one."),
605
618
  },
606
- }, ({ snapshot_id, name, select }, extra) => guarded(async () => {
607
- const c = unwrapComputer(await session.api
619
+ }, ({ snapshot_id, name, select, memory, inherit_secrets }, extra) => guarded(async () => {
620
+ // Each option only when set, so an ordinary clone sends what it always
621
+ // did; consent is sent only when given, since its absence is the default.
622
+ const body = {};
623
+ if (name !== undefined)
624
+ body.name = name;
625
+ if (memory !== undefined)
626
+ body.memory = memory;
627
+ if (inherit_secrets === true)
628
+ body.inherit_secrets = true;
629
+ const answer = await session.api
608
630
  .with(extra.signal)
609
- .json('POST', P.snapshotAction(snapshot_id, 'clone'), {
610
- body: name === undefined ? {} : { name },
611
- }));
631
+ .json('POST', P.snapshotAction(snapshot_id, 'clone'), { body });
632
+ const c = unwrapComputer(answer);
612
633
  if (!c.id) {
613
634
  return refused(`The platform accepted the clone of ${snapshot_id} but sent no computer id back, so the copy cannot be identified. It may exist and be billable — list_computers will say. The selected computer is unchanged.`, withoutCredentials(c));
614
635
  }
615
636
  if (select && c.id)
616
637
  session.bind(c.id, c.resolution);
617
- return said(`Forked ${snapshot_id} into ${describe(c)}${select && c.id ? ', and selected it' : ''}.`, withoutCredentials(c));
638
+ // The platform built it from the disk when the session could not come
639
+ // across (platform OPL-4964). Said first and in words, because a model
640
+ // that asked for a live fork and got a fresh boot would otherwise go on
641
+ // as if the session — its open windows, its running programs — were
642
+ // there.
643
+ //
644
+ // Read off the ANSWER as well as the computer: the platform sends the
645
+ // two fields beside the computer's own, flat, and unwrapComputer keeps
646
+ // only start_error from an envelope — so a future envelope carrying
647
+ // them would otherwise report a live fork for a fresh boot.
648
+ const outer = (answer && typeof answer === 'object' ? answer : {});
649
+ const raw = { ...c, ...outer };
650
+ if (raw.memory_dropped === true) {
651
+ const why = raw.memory_dropped_reason === 'bindings unrecorded'
652
+ ? "it was taken before copies could be given its computer's secrets"
653
+ : raw.memory_dropped_reason === 'secrets'
654
+ ? 'its computer held secrets and inherit_secrets was not set'
655
+ : 'the platform did not resume it';
656
+ return said(`Built ${describe(c)} from ${snapshot_id}'s DISK, booting fresh — its saved session was NOT resumed, because ${why}. No secrets are bound to it${select && c.id ? '. It is selected' : ''}.`, withoutCredentials(c));
657
+ }
658
+ return said(`${memory === false ? 'Built' : 'Forked'} ${snapshot_id} into ${describe(c)}${memory === false ? ' from its disk' : ''}${select && c.id ? ', and selected it' : ''}.`, withoutCredentials(c));
618
659
  }));
619
660
  server.registerTool('delete_snapshot', {
620
661
  title: 'Delete a snapshot',
@@ -725,67 +766,72 @@ export const registerSnapshots = (server, session, opts) => {
725
766
  `wait to find out. ${byHand}`, accepted);
726
767
  }
727
768
  const beat = heartbeat(extra, server.server);
728
- await beat(`Deleting ${snapshot_id} — the platform has accepted it.`);
729
- let blocked;
730
- let seen;
731
- while (untilDeadline && !untilDeadline.aborted) {
732
- if (extra.signal?.aborted) {
733
- return refused(`Cancelled while waiting for ${snapshot_id} to be deleted. THE DELETION IS STILL RUNNING — ` +
734
- `nothing was called back, and objects it has already removed are already gone. ${byHand}`, accepted);
735
- }
736
- // `unfinished`, because the state a stalled deletion sits in is the
737
- // one a bare listing hides. Asking without it would read a snapshot
738
- // stuck half-deleted as a snapshot successfully deleted, which is the
739
- // one wrong answer this tool must not give.
740
- const turn = await snapshotTurn(api, snapshot_id, true);
741
- if (extra.signal?.aborted)
742
- continue;
743
- if (turn.kind === 'cancelled') {
744
- if (untilDeadline.aborted)
745
- break;
746
- blocked = turn.why;
747
- await beat(`Deleting ${snapshot_id} — the platform could not be asked: ${turn.why}`);
769
+ try {
770
+ await beat(`Deleting ${snapshot_id} — the platform has accepted it.`);
771
+ let blocked;
772
+ let seen;
773
+ while (untilDeadline && !untilDeadline.aborted) {
774
+ if (extra.signal?.aborted) {
775
+ return refused(`Cancelled while waiting for ${snapshot_id} to be deleted. THE DELETION IS STILL RUNNING — ` +
776
+ `nothing was called back, and objects it has already removed are already gone. ${byHand}`, accepted);
777
+ }
778
+ // `unfinished`, because the state a stalled deletion sits in is the
779
+ // one a bare listing hides. Asking without it would read a snapshot
780
+ // stuck half-deleted as a snapshot successfully deleted, which is the
781
+ // one wrong answer this tool must not give.
782
+ const turn = await snapshotTurn(api, snapshot_id, true);
783
+ if (extra.signal?.aborted)
784
+ continue;
785
+ if (turn.kind === 'cancelled') {
786
+ if (untilDeadline.aborted)
787
+ break;
788
+ blocked = turn.why;
789
+ await beat(`Deleting ${snapshot_id} — the platform could not be asked: ${turn.why}`);
790
+ await sleep(POLL_MS, signal);
791
+ continue;
792
+ }
793
+ if (turn.kind === 'broken') {
794
+ return refused(`${turn.why}\n\nTHE DELETION IS STILL RUNNING — this was the poll failing, not the ` +
795
+ `deletion. ${byHand}`, accepted);
796
+ }
797
+ if (turn.kind === 'blocked') {
798
+ blocked = turn.why;
799
+ await beat(`Deleting ${snapshot_id} — the platform could not be asked: ${turn.why}`);
800
+ await sleep(turn.after ?? POLL_MS, signal);
801
+ continue;
802
+ }
803
+ // The row is gone from a listing that was read whole and that ASKED
804
+ // for the unfinished ones. Both halves are what make this sentence
805
+ // true rather than merely likely.
806
+ if (turn.kind === 'absent')
807
+ return said(`Deleted snapshot ${snapshot_id}.`, accepted);
808
+ blocked = undefined;
809
+ seen = typeof turn.row.state === 'string' ? turn.row.state : undefined;
810
+ await beat(`Deleting ${snapshot_id} — still listed${seen ? `, reading "${seen}"` : ''}.`);
748
811
  await sleep(POLL_MS, signal);
749
- continue;
750
- }
751
- if (turn.kind === 'broken') {
752
- return refused(`${turn.why}\n\nTHE DELETION IS STILL RUNNING — this was the poll failing, not the ` +
753
- `deletion. ${byHand}`, accepted);
754
812
  }
755
- if (turn.kind === 'blocked') {
756
- blocked = turn.why;
757
- await beat(`Deleting ${snapshot_id} — the platform could not be asked: ${turn.why}`);
758
- await sleep(turn.after ?? POLL_MS, signal);
759
- continue;
760
- }
761
- // The row is gone from a listing that was read whole and that ASKED
762
- // for the unfinished ones. Both halves are what make this sentence
763
- // true rather than merely likely.
764
- if (turn.kind === 'absent')
765
- return said(`Deleted snapshot ${snapshot_id}.`, accepted);
766
- blocked = undefined;
767
- seen = typeof turn.row.state === 'string' ? turn.row.state : undefined;
768
- await beat(`Deleting ${snapshot_id} — still listed${seen ? `, reading "${seen}"` : ''}.`);
769
- await sleep(POLL_MS, signal);
813
+ // Still listed. Three different things that can mean, and they are not
814
+ // one sentence: the platform could not be asked, the deletion got part
815
+ // way and stalled, or it never got started on — which is the shape the
816
+ // one conflict that arrives AFTER the 202 leaves behind, a dependent
817
+ // that is itself being deleted and so cannot be detached.
818
+ return refused(blocked
819
+ ? `Gave up watching after ${timeout_s}s; the platform could not be asked whether ${snapshot_id} ` +
820
+ `is gone — the last attempt said: ${blocked}. THE DELETION IS STILL RUNNING. ${byHand}`
821
+ : seen === DELETING
822
+ ? `${snapshot_id} is still listed after ${timeout_s}s and reads "${DELETING}": the deletion ` +
823
+ `started and has not finished. Nothing was undone by giving up on the wait, and the ` +
824
+ `platform retries a stalled deletion itself every fifteen minutes — so this usually needs ` +
825
+ `watching rather than repeating. ${byHand}`
826
+ : `${snapshot_id} is still listed after ${timeout_s}s${seen ? `, reading "${seen}"` : ''} — ` +
827
+ `the deletion has not reached the point of marking it "${DELETING}". That is what a big ` +
828
+ `chain looks like early on, and it is also what the one conflict that arrives after the ` +
829
+ `deletion is accepted looks like: a dependent snapshot that is ITSELF being deleted cannot ` +
830
+ `be detached, so this one waits for that one. Nothing was destroyed either way. ${byHand}`, accepted);
831
+ }
832
+ finally {
833
+ await beat.stop();
770
834
  }
771
- // Still listed. Three different things that can mean, and they are not
772
- // one sentence: the platform could not be asked, the deletion got part
773
- // way and stalled, or it never got started on — which is the shape the
774
- // one conflict that arrives AFTER the 202 leaves behind, a dependent
775
- // that is itself being deleted and so cannot be detached.
776
- return refused(blocked
777
- ? `Gave up watching after ${timeout_s}s; the platform could not be asked whether ${snapshot_id} ` +
778
- `is gone — the last attempt said: ${blocked}. THE DELETION IS STILL RUNNING. ${byHand}`
779
- : seen === DELETING
780
- ? `${snapshot_id} is still listed after ${timeout_s}s and reads "${DELETING}": the deletion ` +
781
- `started and has not finished. Nothing was undone by giving up on the wait, and the ` +
782
- `platform retries a stalled deletion itself every fifteen minutes — so this usually needs ` +
783
- `watching rather than repeating. ${byHand}`
784
- : `${snapshot_id} is still listed after ${timeout_s}s${seen ? `, reading "${seen}"` : ''} — ` +
785
- `the deletion has not reached the point of marking it "${DELETING}". That is what a big ` +
786
- `chain looks like early on, and it is also what the one conflict that arrives after the ` +
787
- `deletion is accepted looks like: a dependent snapshot that is ITSELF being deleted cannot ` +
788
- `be detached, so this one waits for that one. Nothing was destroyed either way. ${byHand}`, accepted);
789
835
  }));
790
836
  };
791
837
  //# sourceMappingURL=snapshots.js.map