@aragon-agent/cli 0.6.38 → 0.6.39

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 (156) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/registry.d.ts +39 -0
  14. package/dist/commands/task-command.d.ts +39 -0
  15. package/dist/commands/task-command.js +102 -0
  16. package/dist/config/cli-commands.d.ts +23 -0
  17. package/dist/config/cli-commands.js +36 -0
  18. package/dist/config/env.js +17 -4
  19. package/dist/config/load.js +6 -1
  20. package/dist/config/schema.d.ts +43 -4
  21. package/dist/config/schema.js +31 -5
  22. package/dist/config/store.js +17 -2
  23. package/dist/diagnostics/doctor.d.ts +9 -0
  24. package/dist/diagnostics/doctor.js +34 -0
  25. package/dist/diagnostics/info.d.ts +71 -0
  26. package/dist/diagnostics/info.js +91 -5
  27. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  28. package/dist/exec/bridge/caller-tools.js +213 -0
  29. package/dist/exec/bridge/decisions.d.ts +78 -0
  30. package/dist/exec/bridge/decisions.js +116 -0
  31. package/dist/exec/bridge/human-answers.d.ts +38 -0
  32. package/dist/exec/bridge/human-answers.js +164 -0
  33. package/dist/exec/bridge/limits.d.ts +68 -0
  34. package/dist/exec/bridge/limits.js +68 -0
  35. package/dist/exec/bridge/ndjson.d.ts +17 -0
  36. package/dist/exec/bridge/ndjson.js +83 -0
  37. package/dist/exec/bridge/pending.d.ts +70 -0
  38. package/dist/exec/bridge/pending.js +132 -0
  39. package/dist/exec/bridge/protocol.d.ts +346 -0
  40. package/dist/exec/bridge/protocol.js +256 -0
  41. package/dist/exec/bridge/server.d.ts +142 -0
  42. package/dist/exec/bridge/server.js +502 -0
  43. package/dist/exec/bridge/source.d.ts +58 -0
  44. package/dist/exec/bridge/source.js +145 -0
  45. package/dist/exec/bridge/writer.d.ts +26 -0
  46. package/dist/exec/bridge/writer.js +156 -0
  47. package/dist/exec/cli-commands.js +30 -2
  48. package/dist/exec/emitter.d.ts +16 -2
  49. package/dist/exec/emitter.js +26 -6
  50. package/dist/exec/events.d.ts +465 -10
  51. package/dist/exec/events.js +47 -1
  52. package/dist/exec/hooks/dispatch.d.ts +76 -0
  53. package/dist/exec/hooks/dispatch.js +169 -0
  54. package/dist/exec/hooks/index.d.ts +83 -0
  55. package/dist/exec/hooks/index.js +205 -0
  56. package/dist/exec/hooks/limits.d.ts +56 -0
  57. package/dist/exec/hooks/limits.js +49 -0
  58. package/dist/exec/hooks/registry.d.ts +49 -0
  59. package/dist/exec/hooks/registry.js +91 -0
  60. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  61. package/dist/exec/hooks/tool-wrapper.js +154 -0
  62. package/dist/exec/hooks/turn.d.ts +61 -0
  63. package/dist/exec/hooks/turn.js +59 -0
  64. package/dist/exec/hooks/validate.d.ts +85 -0
  65. package/dist/exec/hooks/validate.js +310 -0
  66. package/dist/exec/index.d.ts +130 -4
  67. package/dist/exec/index.js +501 -10
  68. package/dist/exec/options.d.ts +124 -2
  69. package/dist/exec/options.js +413 -6
  70. package/dist/exec/permission.d.ts +9 -1
  71. package/dist/exec/permission.js +7 -1
  72. package/dist/exec/request/limits.d.ts +51 -0
  73. package/dist/exec/request/limits.js +49 -0
  74. package/dist/exec/request/scope.d.ts +106 -0
  75. package/dist/exec/request/scope.js +149 -0
  76. package/dist/exec/result/limits.d.ts +74 -0
  77. package/dist/exec/result/limits.js +72 -0
  78. package/dist/exec/result/prompt.d.ts +28 -0
  79. package/dist/exec/result/prompt.js +74 -0
  80. package/dist/exec/result/schema.d.ts +99 -0
  81. package/dist/exec/result/schema.js +464 -0
  82. package/dist/exec/result/sink.d.ts +56 -0
  83. package/dist/exec/result/sink.js +50 -0
  84. package/dist/exec/result/tool.d.ts +37 -0
  85. package/dist/exec/result/tool.js +118 -0
  86. package/dist/exec/runner.d.ts +395 -1
  87. package/dist/exec/runner.js +860 -8
  88. package/dist/exec/threads/aggregate.d.ts +34 -0
  89. package/dist/exec/threads/aggregate.js +30 -0
  90. package/dist/exec/threads/capacity.d.ts +23 -0
  91. package/dist/exec/threads/capacity.js +38 -0
  92. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  93. package/dist/exec/threads/dispatch-source.js +106 -0
  94. package/dist/exec/threads/execute-threads.d.ts +53 -0
  95. package/dist/exec/threads/execute-threads.js +234 -0
  96. package/dist/exec/threads/gate.d.ts +42 -0
  97. package/dist/exec/threads/gate.js +81 -0
  98. package/dist/exec/threads/identity.d.ts +36 -0
  99. package/dist/exec/threads/identity.js +67 -0
  100. package/dist/exec/threads/interrupt.d.ts +29 -0
  101. package/dist/exec/threads/interrupt.js +79 -0
  102. package/dist/exec/threads/limits.d.ts +58 -0
  103. package/dist/exec/threads/limits.js +35 -0
  104. package/dist/exec/threads/outcome.d.ts +47 -0
  105. package/dist/exec/threads/outcome.js +26 -0
  106. package/dist/exec/threads/registry.d.ts +81 -0
  107. package/dist/exec/threads/registry.js +427 -0
  108. package/dist/exec/threads/runtime.d.ts +107 -0
  109. package/dist/exec/threads/runtime.js +523 -0
  110. package/dist/exec/threads/thread-source.d.ts +20 -0
  111. package/dist/exec/threads/thread-source.js +134 -0
  112. package/dist/fast/limits.d.ts +54 -0
  113. package/dist/fast/limits.js +54 -0
  114. package/dist/fast/summary-call.d.ts +72 -0
  115. package/dist/fast/summary-call.js +133 -0
  116. package/dist/fast/summary-prompt.d.ts +106 -0
  117. package/dist/fast/summary-prompt.js +320 -0
  118. package/dist/fast/task-summary-types.d.ts +66 -0
  119. package/dist/fast/task-summary-types.js +17 -0
  120. package/dist/logging/logger.d.ts +1 -1
  121. package/dist/proc/limits.d.ts +7 -0
  122. package/dist/proc/limits.js +7 -0
  123. package/dist/proc/output-streams.d.ts +50 -0
  124. package/dist/proc/output-streams.js +54 -0
  125. package/dist/team/runtime.d.ts +9 -0
  126. package/dist/team/runtime.js +8 -0
  127. package/dist/team/subagent.d.ts +15 -0
  128. package/dist/team/subagent.js +45 -1
  129. package/dist/tools/bash-tool.d.ts +10 -0
  130. package/dist/tools/bash-tool.js +71 -6
  131. package/dist/tools/index.d.ts +93 -1
  132. package/dist/tools/index.js +112 -3
  133. package/dist/tools/tool-gate.d.ts +100 -0
  134. package/dist/tools/tool-gate.js +87 -0
  135. package/dist/ui/App.js +224 -11
  136. package/dist/ui/Header.d.ts +28 -16
  137. package/dist/ui/Header.js +21 -19
  138. package/dist/ui/interaction-hints.js +6 -0
  139. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  140. package/dist/ui/layout/OverlayFrame.js +19 -4
  141. package/dist/ui/layout/header-hints.d.ts +55 -62
  142. package/dist/ui/layout/header-hints.js +59 -166
  143. package/dist/ui/layout/header-left.d.ts +115 -0
  144. package/dist/ui/layout/header-left.js +206 -0
  145. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  146. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  147. package/dist/ui/layout/status-detail-layout.js +5 -1
  148. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  149. package/dist/ui/layout/task-summary-cell.js +65 -0
  150. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  151. package/dist/ui/overlays/HelpOverlay.js +29 -2
  152. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  153. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  154. package/dist/ui/shortcuts.d.ts +81 -0
  155. package/dist/ui/shortcuts.js +221 -0
  156. package/package.json +2 -2
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The admission gate: how many threads may hold a request in flight
3
+ * (headless-threads-and-lifecycle-hooks section 2.3.5).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * A COUNTING SEMAPHORE WITH THREE RULES, and no fourth:
8
+ * - `acquire` resolves when a slot is free, FIFO;
9
+ * - `release` frees one slot, once;
10
+ * - `close` makes every waiter (and every later acquirer) throw, so a process
11
+ * that is shutting down cannot be parked behind a slot that will never come.
12
+ *
13
+ * WHY FIFO RATHER THAN "WHOEVER ASKS NEXT": a caller that opened eight tenants in
14
+ * order expects them served in order. Preemption would let a noisy tenant starve a
15
+ * quiet one forever, and the slot is released at REQUEST granularity (not thread
16
+ * granularity), so fairness is cheap here.
17
+ *
18
+ * IT DOES NOT KNOW WHAT A REQUEST IS. The holder decides when to release; the gate
19
+ * only counts. That is what keeps this file 100 lines instead of a scheduler.
20
+ */
21
+ export class GateClosedError extends Error {
22
+ constructor(message = 'The admission gate is closed; no further requests are admitted.') {
23
+ super(message);
24
+ this.name = 'GateClosedError';
25
+ }
26
+ }
27
+ export function createAdmissionGate(options) {
28
+ const limit = Math.max(1, Math.trunc(options.limit));
29
+ /** Who holds a slot, so a double release is detectable rather than silent. */
30
+ const holders = new Set();
31
+ let waiters = [];
32
+ let closed = false;
33
+ const admit = (threadId) => {
34
+ holders.add(threadId);
35
+ };
36
+ /** Hand the next queued waiter the slot that was just freed. */
37
+ const pump = () => {
38
+ while (holders.size < limit && waiters.length > 0) {
39
+ const next = waiters.shift();
40
+ if (!next)
41
+ return;
42
+ admit(next.threadId);
43
+ next.resolve();
44
+ }
45
+ };
46
+ return {
47
+ acquire(threadId) {
48
+ if (closed)
49
+ return Promise.reject(new GateClosedError());
50
+ if (holders.has(threadId)) {
51
+ // ONE SLOT PER THREAD, NOT PER REQUEST. A thread can only ever have one
52
+ // request in flight (its source hands out one prompt at a time), so a second
53
+ // acquire for the same id is a re-entrant call, not a second request.
54
+ return Promise.resolve();
55
+ }
56
+ if (holders.size < limit) {
57
+ admit(threadId);
58
+ return Promise.resolve();
59
+ }
60
+ return new Promise((resolve, reject) => {
61
+ waiters.push({ threadId, resolve, reject });
62
+ });
63
+ },
64
+ release(threadId) {
65
+ if (!holders.delete(threadId)) {
66
+ throw new Error(`release("${threadId}") without a matching acquire`);
67
+ }
68
+ pump();
69
+ },
70
+ close() {
71
+ if (closed)
72
+ return;
73
+ closed = true;
74
+ const parked = waiters;
75
+ waiters = [];
76
+ for (const waiter of parked)
77
+ waiter.reject(new GateClosedError());
78
+ },
79
+ inUse: () => holders.size,
80
+ };
81
+ }
@@ -0,0 +1,36 @@
1
+ /** The thread mode's own name for its first thread. Not configurable. */
2
+ export declare const DEFAULT_THREAD_ID = "main";
3
+ /**
4
+ * THE WIRE CODES, spelled exactly as section 4.2's ack table spells them (`thread_id_invalid`).
5
+ * A caller switches on these strings; renaming them is a protocol change, not a refactor.
6
+ */
7
+ export type ThreadIdProblem = 'thread_id_invalid' | 'thread_id_too_long';
8
+ export type ThreadSessionIdResult = {
9
+ ok: true;
10
+ threadId: string;
11
+ sessionId: string;
12
+ derived: boolean;
13
+ } | {
14
+ ok: false;
15
+ code: ThreadIdProblem;
16
+ };
17
+ export interface ThreadIdentityInput {
18
+ threadId: string;
19
+ /** The process-level `--session-id` (or `plan.id`); the base of every derived id. */
20
+ baseSessionId: string;
21
+ /** `thread_open{sessionId}`, already trimmed. */
22
+ explicitSessionId?: string;
23
+ /** `--no-save-session`. Kept as a parameter so the caller states the intent once. */
24
+ saving: boolean;
25
+ }
26
+ export declare function isValidThreadId(threadId: string): boolean;
27
+ /**
28
+ * Resolve the session id a thread writes.
29
+ *
30
+ * THE THREE CASES, in the order the design fixes them:
31
+ * 1. an explicit `sessionId` wins, and must be a legal session id;
32
+ * 2. the DEFAULT thread reuses the process id unchanged, so `--session-id` keeps
33
+ * meaning what it meant before this feature existed;
34
+ * 3. any other thread derives `${base}-${threadId}`.
35
+ */
36
+ export declare function resolveThreadIdentity(input: ThreadIdentityInput): ThreadSessionIdResult;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `threadId` -> session id derivation (headless-threads-and-lifecycle-hooks
3
+ * section 2.3.6).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * PURE. No disk, no clock, no session lock: this module answers "which file would
8
+ * this thread write" and "is the answer even legal", so the registry can refuse a
9
+ * `thread_open` BEFORE a controller is constructed. That ordering is the point -
10
+ * a half-built controller whose session file could not be acquired is a leak with
11
+ * no owner (section 2.3.4, step 1).
12
+ *
13
+ * NEVER TRUNCATE. A derived id that is too long is REJECTED, not clipped: two long
14
+ * thread ids that both clipped to the same 64 characters would share one file, and
15
+ * "two conversations write one file" is the exact defect this feature exists to
16
+ * remove.
17
+ */
18
+ import { isValidSessionId } from '../../session/store.js';
19
+ import { THREAD_LIMITS } from './limits.js';
20
+ /** `[a-z0-9][a-z0-9_-]{0,31}` - deliberately narrower than a session id. */
21
+ const THREAD_ID_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
22
+ /** The thread mode's own name for its first thread. Not configurable. */
23
+ export const DEFAULT_THREAD_ID = 'main';
24
+ export function isValidThreadId(threadId) {
25
+ return threadId.length <= THREAD_LIMITS.maxThreadIdLength && THREAD_ID_RE.test(threadId);
26
+ }
27
+ /**
28
+ * Resolve the session id a thread writes.
29
+ *
30
+ * THE THREE CASES, in the order the design fixes them:
31
+ * 1. an explicit `sessionId` wins, and must be a legal session id;
32
+ * 2. the DEFAULT thread reuses the process id unchanged, so `--session-id` keeps
33
+ * meaning what it meant before this feature existed;
34
+ * 3. any other thread derives `${base}-${threadId}`.
35
+ */
36
+ export function resolveThreadIdentity(input) {
37
+ if (!isValidThreadId(input.threadId)) {
38
+ return { ok: false, code: 'thread_id_invalid' };
39
+ }
40
+ if (input.explicitSessionId !== undefined) {
41
+ if (!isValidSessionId(input.explicitSessionId)) {
42
+ return { ok: false, code: 'thread_id_invalid' };
43
+ }
44
+ return {
45
+ ok: true,
46
+ threadId: input.threadId,
47
+ sessionId: input.explicitSessionId,
48
+ derived: false,
49
+ };
50
+ }
51
+ if (input.threadId === DEFAULT_THREAD_ID) {
52
+ // `plan.id` IS ALREADY LEGAL and already means "this run's conversation", so
53
+ // re-deriving it would silently rename every existing session on upgrade.
54
+ return { ok: true, threadId: input.threadId, sessionId: input.baseSessionId, derived: false };
55
+ }
56
+ const derived = `${input.baseSessionId}-${input.threadId}`;
57
+ if (!isValidSessionId(derived)) {
58
+ // A length overflow is the only cause that is not the threadId's own fault (the
59
+ // threadId was checked above), and the two need different advice: shorten the
60
+ // base, versus pick another name.
61
+ return {
62
+ ok: false,
63
+ code: derived.length > 64 ? 'thread_id_too_long' : 'thread_id_invalid',
64
+ };
65
+ }
66
+ return { ok: true, threadId: input.threadId, sessionId: derived, derived: true };
67
+ }
@@ -0,0 +1,29 @@
1
+ import type { ExecStopReason } from '../events.js';
2
+ /** Everything the owner needs from a running thread run. Kept structural on purpose. */
3
+ export interface InterruptFanout {
4
+ /** Stop every open thread's request, keeping each thread's own settle path. */
5
+ interruptAll(signo: number): void;
6
+ /** Resolves once every opened thread has closed. */
7
+ drained(): Promise<void>;
8
+ }
9
+ export interface ProcessInterruptPort {
10
+ /** Install the ONE process-level terminator. Called once by `executeThreads`. */
11
+ install(): void;
12
+ /**
13
+ * The same fan-out a real signal performs, without a signal: the `interrupt` frame
14
+ * and `--max-duration` both need it, and both must produce the same shutdown.
15
+ */
16
+ requestInterrupt(signo: number): void;
17
+ /** True once a reason has been recorded; `executeThreads` reads it for `result`. */
18
+ signo(): number | null;
19
+ }
20
+ export interface InterruptOwnerDeps {
21
+ fanout: InterruptFanout;
22
+ /** Write the process `result` (idempotent; the emitter's `done` latch guards it). */
23
+ ensureResult: (stopReason: ExecStopReason) => void;
24
+ /** Test injection. Production is `process.exit`. */
25
+ exit?: (code: number) => void;
26
+ /** Test injection. Production is `setSignalTerminator`. */
27
+ setTerminator?: (fn: (signo: number) => void) => void;
28
+ }
29
+ export declare function createInterruptOwner(deps: InterruptOwnerDeps): ProcessInterruptPort;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The ONE owner of the process-level signal, and the fan-out to every thread
3
+ * (headless-threads-and-lifecycle-hooks section 2.3.8a; review P0-1).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * WHY THIS EXISTS AT ALL. `ExecRunner.attach()` ends with `installTerminator()`, and
8
+ * that installs into `logging/install.ts`'s SINGLE process-wide slot. With N runners
9
+ * the last one to attach wins, so Ctrl-C would abort one controller and leave the
10
+ * others running - and a second signal would run `process.exit` from inside a runner,
11
+ * which writes no `result` at all. Both are silent in single-thread use and fatal in
12
+ * multi-tenant use, so ownership is lifted out here and made a TYPE:
13
+ *
14
+ * - thread runners receive `installSignalTerminator: false` (they install nothing);
15
+ * - `executeThreads` calls `install()` EXACTLY ONCE;
16
+ * - the terminator fans out through the registry, never through a controller.
17
+ *
18
+ * THE ORDER OF THE LAST THREE STATEMENTS IS THE CONTRACT: settle the threads, write
19
+ * `result`, and only then exit. That is the same "persist first, announce second"
20
+ * discipline `exec/index.ts` already follows, applied to the exit code.
21
+ */
22
+ import { setSignalTerminator } from '../../logging/install.js';
23
+ export function createInterruptOwner(deps) {
24
+ const exit = deps.exit ?? ((code) => process.exit(code));
25
+ const setTerminator = deps.setTerminator ?? setSignalTerminator;
26
+ let signo = null;
27
+ let settled = false;
28
+ /**
29
+ * The hard exit: `result` first, then leave.
30
+ *
31
+ * `try/finally`, NOT A SEQUENCE: `ensureResult` builds the aggregate from every thread's
32
+ * account, and anything it touches can throw on a half-disposed runtime. Leaving the
33
+ * process alive because the exit code could not be written would be the worst of both
34
+ * outcomes - no `result` AND no exit - so the exit is unconditional.
35
+ */
36
+ const exitHard = (signal) => {
37
+ try {
38
+ deps.ensureResult('interrupted');
39
+ }
40
+ finally {
41
+ exit(128 + signal);
42
+ }
43
+ };
44
+ const begin = (signal) => {
45
+ if (settled) {
46
+ // A WEDGED SETTLE IS ALWAYS ONE MORE CTRL-C AWAY FROM ENDING - the property the
47
+ // single-run path already has - but `result` is attempted first, so "at least one
48
+ // result line" is structural instead of hopeful. `settled` alone is the test: it is
49
+ // set by the first signal, so "a later signal" and "a second one" are the same fact
50
+ // and a timing window would only be a second, weaker copy of it.
51
+ exitHard(signal);
52
+ return;
53
+ }
54
+ settled = true;
55
+ signo = signal;
56
+ deps.fanout.interruptAll(signal);
57
+ // ASYNCHRONOUS ON PURPOSE: the terminator itself must return to the event loop so
58
+ // the aborts and the settle can run. Nothing awaits this promise; the exit at the
59
+ // end of it is the completion signal.
60
+ // THE CHAIN CANNOT LEAVE THE PROCESS WEDGED (review P1). Every step is fallible -
61
+ // `drained()` rejects when a thread's controller throws while closing, and
62
+ // `ensureResult` walks every account - and an unhandled rejection here would be turned
63
+ // into `handleFatal` by `logging/install.ts`, i.e. exit 1 with NO `result` line on
64
+ // stdout. The failure path does the same two things as the success path.
65
+ void deps.fanout
66
+ .drained()
67
+ .then(() => deps.ensureResult('interrupted'), () => deps.ensureResult('interrupted'))
68
+ .then(() => exit(128 + signal), () => exit(128 + signal));
69
+ };
70
+ return {
71
+ install() {
72
+ setTerminator((signal) => begin(signal));
73
+ },
74
+ requestInterrupt(signal) {
75
+ begin(signal);
76
+ },
77
+ signo: () => signo,
78
+ };
79
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Structural bounds for thread multiplexing (headless-threads-and-lifecycle-hooks
3
+ * section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * STRUCTURE, NOT POLICY. These are what one process can physically carry; the
8
+ * user-tunable ceilings (`--max-open-threads`, `--thread-concurrency`) live in
9
+ * `exec/options.ts` and are clamped against the numbers below. `limits.ts` is
10
+ * structural and the flag parser is policy - the split every other subsystem in
11
+ * this package already follows (`team`, `todo`, `proc`, `input`).
12
+ */
13
+ import type { ExecStopReason } from '../events.js';
14
+ export declare const THREAD_LIMITS: {
15
+ /** The hard ceiling on simultaneously open threads, including `main`. */
16
+ readonly maxOpenThreads: 8;
17
+ /** `--max-open-threads`' default. Four controllers fit in one process comfortably. */
18
+ readonly defaultMaxOpenThreads: 4;
19
+ /** `--thread-concurrency`' default: how many threads may hold a request in flight. */
20
+ readonly defaultConcurrency: 2;
21
+ /**
22
+ * A threadId's own length ceiling. `SESSION_ID_RE` allows 64 characters for a
23
+ * session id, and a derived id is `${base}-${threadId}`, so the thread half has
24
+ * to leave room for the base. 32 is what the design fixed and what the README
25
+ * documents.
26
+ */
27
+ readonly maxThreadIdLength: 32;
28
+ /**
29
+ * How many requests one thread may serve before it must be reopened. A guard, not
30
+ * a policy: a per-request `RequestScope` is discarded on every settlement, so this
31
+ * only bounds the thread's own cumulative bookkeeping.
32
+ */
33
+ readonly maxRequestsPerThread: 100000;
34
+ };
35
+ export type ThreadLimits = typeof THREAD_LIMITS;
36
+ /**
37
+ * WHY THE THREAD STOPPED. A closed set, disjoint from `ExecStopReason` on purpose:
38
+ * adding members to that union breaks every exhaustive `switch` downstream, and a
39
+ * thread's reason is a different question from the run's reason (section 2.3.8).
40
+ */
41
+ export type ExecThreadEndReason =
42
+ /** The caller closed it (settle), or the caller's input ended. */
43
+ 'closed'
44
+ /** `thread_close{mode:'abort'}`, or a process-level interrupt. */
45
+ | 'aborted'
46
+ /** `--max-duration` fired while this thread had work in flight. */
47
+ | 'timeout'
48
+ /** Preflight failed, the agent threw, or open failed after `opened`. */
49
+ | 'error';
50
+ /**
51
+ * The ONLY mapping from a thread reason to the process-level union (section 2.3.8b).
52
+ *
53
+ * `aborted` maps to `interrupted` because a process-level interrupt is the only way
54
+ * a thread reaches that reason; a caller's own `thread_close{abort}` leaves the
55
+ * PROCESS reason alone, which is why `decideProcessOutcome` reads the interrupt box
56
+ * rather than this table for that case.
57
+ */
58
+ export declare const PROCESS_REASON_FOR_THREAD: Readonly<Record<ExecThreadEndReason, ExecStopReason>>;
@@ -0,0 +1,35 @@
1
+ export const THREAD_LIMITS = {
2
+ /** The hard ceiling on simultaneously open threads, including `main`. */
3
+ maxOpenThreads: 8,
4
+ /** `--max-open-threads`' default. Four controllers fit in one process comfortably. */
5
+ defaultMaxOpenThreads: 4,
6
+ /** `--thread-concurrency`' default: how many threads may hold a request in flight. */
7
+ defaultConcurrency: 2,
8
+ /**
9
+ * A threadId's own length ceiling. `SESSION_ID_RE` allows 64 characters for a
10
+ * session id, and a derived id is `${base}-${threadId}`, so the thread half has
11
+ * to leave room for the base. 32 is what the design fixed and what the README
12
+ * documents.
13
+ */
14
+ maxThreadIdLength: 32,
15
+ /**
16
+ * How many requests one thread may serve before it must be reopened. A guard, not
17
+ * a policy: a per-request `RequestScope` is discarded on every settlement, so this
18
+ * only bounds the thread's own cumulative bookkeeping.
19
+ */
20
+ maxRequestsPerThread: 100_000,
21
+ };
22
+ /**
23
+ * The ONLY mapping from a thread reason to the process-level union (section 2.3.8b).
24
+ *
25
+ * `aborted` maps to `interrupted` because a process-level interrupt is the only way
26
+ * a thread reaches that reason; a caller's own `thread_close{abort}` leaves the
27
+ * PROCESS reason alone, which is why `decideProcessOutcome` reads the interrupt box
28
+ * rather than this table for that case.
29
+ */
30
+ export const PROCESS_REASON_FOR_THREAD = {
31
+ closed: 'end_turn',
32
+ aborted: 'interrupted',
33
+ timeout: 'timeout',
34
+ error: 'error',
35
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * THE ONE PLACE A PROCESS-LEVEL STOP REASON AND EXIT CODE ARE DECIDED
3
+ * (headless-threads-and-lifecycle-hooks section 2.3.8b, I-T11).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * WHY THIS FILE HAS TO EXIST. Without threads, `ExecRunner.exitCode()` is the only
8
+ * arbiter: one runner, one `stopReason`, one number. With N threads there are N
9
+ * runners, each with its own opinion, and `result` can only be written once. Letting
10
+ * each runner decide would make the reported outcome depend on which thread happened
11
+ * to finish last - the least reproducible failure mode there is.
12
+ *
13
+ * `ExecStopReason` IS UNCHANGED. `error` is the member this round reuses to say "every
14
+ * thread failed"; adding a member would break every exhaustive `switch` downstream.
15
+ */
16
+ import type { ExecStopReason } from '../events.js';
17
+ import type { ThreadAccount } from './aggregate.js';
18
+ export interface ProcessOutcomeInput {
19
+ /** The signal that arrived, or `null`. Set by the one process-level terminator. */
20
+ interrupt: {
21
+ signo: number;
22
+ } | null;
23
+ /** Any thread ended with `reason: 'timeout'`. */
24
+ timedOut: boolean;
25
+ /** The writer's `caller_not_reading`, which is process-level by nature. */
26
+ hardLimited: boolean;
27
+ accounts: readonly ThreadAccount[];
28
+ }
29
+ export interface ProcessOutcome {
30
+ stopReason: ExecStopReason;
31
+ exitCode: number;
32
+ }
33
+ /**
34
+ * Priority is ORDERED; the first hit wins.
35
+ *
36
+ * `hardLimited` leads because the caller cannot read the answer at all: reporting
37
+ * `end_turn` to a pipe nobody is draining would be a lie the next `write()` could not
38
+ * correct. `interrupt` follows because the user's intent outranks a budget. Then the
39
+ * clock, and only then the per-thread verdicts.
40
+ *
41
+ * THE PARAMETER IS `facts`, NOT `input`: this package has a static scanner
42
+ * (`paste-frames.test.ts`) that flags anything named `input` being appended to a string -
43
+ * a heuristic for a raw-keystroke bug that fires on any `.input` access near a string
44
+ * concatenation. Renaming the bag is cheaper and clearer than teaching the scanner an
45
+ * exception.
46
+ */
47
+ export declare function decideProcessOutcome(facts: ProcessOutcomeInput): ProcessOutcome;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Priority is ORDERED; the first hit wins.
3
+ *
4
+ * `hardLimited` leads because the caller cannot read the answer at all: reporting
5
+ * `end_turn` to a pipe nobody is draining would be a lie the next `write()` could not
6
+ * correct. `interrupt` follows because the user's intent outranks a budget. Then the
7
+ * clock, and only then the per-thread verdicts.
8
+ *
9
+ * THE PARAMETER IS `facts`, NOT `input`: this package has a static scanner
10
+ * (`paste-frames.test.ts`) that flags anything named `input` being appended to a string -
11
+ * a heuristic for a raw-keystroke bug that fires on any `.input` access near a string
12
+ * concatenation. Renaming the bag is cheaper and clearer than teaching the scanner an
13
+ * exception.
14
+ */
15
+ export function decideProcessOutcome(facts) {
16
+ if (facts.hardLimited)
17
+ return { stopReason: 'error', exitCode: 1 };
18
+ if (facts.interrupt)
19
+ return { stopReason: 'interrupted', exitCode: 128 + facts.interrupt.signo };
20
+ if (facts.timedOut)
21
+ return { stopReason: 'timeout', exitCode: 3 };
22
+ const allFailed = facts.accounts.length > 0 && facts.accounts.every((a) => a.isError);
23
+ if (allFailed)
24
+ return { stopReason: 'error', exitCode: 1 };
25
+ return { stopReason: 'end_turn', exitCode: 0 };
26
+ }
@@ -0,0 +1,81 @@
1
+ import { DEFAULT_THREAD_ID } from './identity.js';
2
+ import type { AdmissionGate } from './gate.js';
3
+ import type { ThreadAccount } from './aggregate.js';
4
+ import type { ExecThreadEndReason } from './limits.js';
5
+ import { type ThreadRuntime } from './runtime.js';
6
+ import { type ThreadSource } from './thread-source.js';
7
+ import type { BridgeInputFrame, BridgeThreadCloseFrame, BridgeThreadListFrame, BridgeThreadOpenFrame } from '../bridge/protocol.js';
8
+ import type { BridgeServer } from '../bridge/server.js';
9
+ import type { SessionLockHandle } from '../../session/store.js';
10
+ /** One thread as `thread_list` reports it. */
11
+ export interface ThreadSummary {
12
+ threadId: string;
13
+ sessionId: string;
14
+ state: 'opening' | 'open' | 'closing' | 'closed';
15
+ requests: number;
16
+ turns: number;
17
+ inFlight: boolean;
18
+ }
19
+ export interface ThreadRuntimeFactoryInput {
20
+ threadId: string;
21
+ sessionId: string;
22
+ sessionFile: string | null;
23
+ lock: SessionLockHandle | null;
24
+ /**
25
+ * The queue this thread serves. The runtime needs it at construction time, not at
26
+ * `run()`: `runner.attachBridge(server, source)` happens in `open()`, and the bridge
27
+ * channel is what carries every receipt a caller correlates (section 2.3.4 step 5).
28
+ */
29
+ source: ThreadSource;
30
+ }
31
+ export interface ThreadRegistryDeps {
32
+ /** The process-level `--session-id` (or the generated one); every derived id's base. */
33
+ baseSessionId: string;
34
+ saving: boolean;
35
+ maxOpenThreads: number;
36
+ gate: AdmissionGate;
37
+ /** Constructs one thread. Supplied by `executeThreads`, which owns writer/emitter. */
38
+ makeRuntime: (input: ThreadRuntimeFactoryInput) => ThreadRuntime;
39
+ /** One outbound frame. The writer stamps `seq`. */
40
+ write: (event: Record<string, unknown>) => void;
41
+ /** Opens a session lock, or returns `null` when it is taken. */
42
+ acquireLock?: (sessionId: string) => SessionLockHandle | null;
43
+ /** Where a thread's session file lives; `null` under `--no-save-session`. */
44
+ sessionFileFor?: (sessionId: string) => string | null;
45
+ /**
46
+ * Called exactly once, after the first thread's runtime is open and BEFORE its
47
+ * `thread_state{opened}` is written. That is where `executeThreads` emits `system/init`,
48
+ * which must be the first line of the stream.
49
+ */
50
+ onFirstOpen?: () => void;
51
+ }
52
+ export interface ThreadRegistry {
53
+ /** Open the default thread (`main`) and resolve once it is usable. */
54
+ openDefault(): Promise<void>;
55
+ handleOpen(frame: BridgeThreadOpenFrame): void;
56
+ handleClose(frame: BridgeThreadCloseFrame): void;
57
+ handleList(frame: BridgeThreadListFrame): void;
58
+ /** A `prompt` for one thread. Queues while it opens; refuses when it is gone. */
59
+ deliverPrompt(threadId: string, text: string, frameId: string | undefined): void;
60
+ /** A control frame for one thread's server. */
61
+ deliverControl(threadId: string, frame: BridgeInputFrame): void;
62
+ serverFor(threadId: string): BridgeServer | undefined;
63
+ runtimeFor(threadId: string): ThreadRuntime | undefined;
64
+ ids(): string[];
65
+ summaries(): ThreadSummary[];
66
+ /** `interrupt{threadId}`: refused, and the refusal is the whole behaviour (AC-34). */
67
+ rejectThreadedInterrupt(threadId: string, frameId: string | undefined): void;
68
+ /** Every source finishes (the `end` frame, EOF). */
69
+ finishAll(): void;
70
+ /** Resolves once every thread record is `closed`. */
71
+ drained(): Promise<void>;
72
+ /** Process-level shutdown. Returns the accounts in OPEN ORDER. */
73
+ closeAll(reason: ExecThreadEndReason): Promise<ThreadAccount[]>;
74
+ interruptAll(signo: number): void;
75
+ timeoutAll(): void;
76
+ accounts(): ThreadAccount[];
77
+ /** Threads that are not `closed` yet. */
78
+ openCount(): number;
79
+ }
80
+ export declare function createThreadRegistry(deps: ThreadRegistryDeps): ThreadRegistry;
81
+ export { DEFAULT_THREAD_ID };