@aragon-agent/cli 0.6.38 → 0.6.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +263 -0
- package/README.md +544 -11
- package/dist/agent/controller.d.ts +245 -10
- package/dist/agent/controller.js +367 -22
- package/dist/agent/model-profile-settings.js +12 -1
- package/dist/agent/plan-prompt.d.ts +1 -3
- package/dist/agent/plan-prompt.js +13 -7
- package/dist/agent/reducer.d.ts +70 -2
- package/dist/agent/reducer.js +59 -6
- package/dist/agent/skill-triage-prompt.d.ts +42 -0
- package/dist/agent/skill-triage-prompt.js +74 -0
- package/dist/agent/system-prompt.d.ts +28 -2
- package/dist/agent/system-prompt.js +12 -4
- package/dist/cli.js +106 -32
- package/dist/commands/builtins.js +24 -3
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/skills.js +86 -1
- package/dist/commands/subagent-command.d.ts +25 -0
- package/dist/commands/subagent-command.js +242 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/compaction/command.js +16 -3
- package/dist/compaction/compactor.d.ts +25 -0
- package/dist/compaction/compactor.js +111 -10
- package/dist/compaction/digest.d.ts +24 -1
- package/dist/compaction/digest.js +139 -17
- package/dist/compaction/limits.d.ts +21 -0
- package/dist/compaction/limits.js +21 -0
- package/dist/compaction/reason-text.d.ts +36 -0
- package/dist/compaction/reason-text.js +78 -0
- package/dist/compaction/types.d.ts +82 -14
- package/dist/compaction/wiring.d.ts +13 -8
- package/dist/compaction/wiring.js +1 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +55 -3
- package/dist/config/schema.d.ts +125 -10
- package/dist/config/schema.js +138 -14
- package/dist/config/store.js +23 -3
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/log-mirror.d.ts +21 -4
- package/dist/proc/log-mirror.js +29 -7
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/skills/service.d.ts +41 -0
- package/dist/skills/service.js +62 -0
- package/dist/skills/triage.d.ts +194 -0
- package/dist/skills/triage.js +393 -0
- package/dist/team/forced-dispatch.d.ts +95 -0
- package/dist/team/forced-dispatch.js +208 -0
- package/dist/team/limits.d.ts +21 -1
- package/dist/team/limits.js +21 -1
- package/dist/team/normalize.d.ts +15 -0
- package/dist/team/normalize.js +21 -4
- package/dist/team/prompt.d.ts +6 -3
- package/dist/team/prompt.js +9 -5
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/team/task-tool.js +11 -7
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/human-input.d.ts +35 -2
- package/dist/tools/human-input.js +38 -0
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/plan-tools.d.ts +20 -12
- package/dist/tools/plan-tools.js +24 -17
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +238 -12
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/entries/CompactionCard.d.ts +11 -1
- package/dist/ui/entries/CompactionCard.js +65 -4
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/status-layout.d.ts +10 -0
- package/dist/ui/layout/status-layout.js +18 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/SettingsScreen.js +29 -5
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
- package/dist/ui/overlays/TeamFanoutPage.js +78 -0
- package/dist/ui/overlays/model-profile-state.d.ts +19 -1
- package/dist/ui/overlays/model-profile-state.js +12 -0
- package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
- package/dist/ui/overlays/team-fanout-state.js +97 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- 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 };
|