@ctrl-spc/cs 0.6.0 → 0.7.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.
package/dist/env.js CHANGED
@@ -10,6 +10,48 @@ export const SUPABASE_KEY = process.env.CTRL_SPC_SUPABASE_KEY || 'sb_publishable
10
10
  export const HEARTBEAT_INTERVAL_MS = 10_000;
11
11
  /** Command (ping) poll cadence. */
12
12
  export const COMMAND_POLL_INTERVAL_MS = 3_000;
13
+ /** Orchestrator listener cadence (feature 16, Slice 3). The interval a designated
14
+ * machine waits before looking for a new loose todo — and therefore the worst
15
+ * case for ux.md mock state E step 1, "Working with no agent named. Present for
16
+ * at most one poll interval". Matched to the command poll: both are one small
17
+ * indexed read against the user's own rows, and a second cadence to reason
18
+ * about would buy nothing. Not faster — the panel is realtime, so the user sees
19
+ * their own todo appear instantly; this interval only bounds how long before an
20
+ * agent is NAMED on it. */
21
+ export const ORCHESTRATOR_POLL_INTERVAL_MS = 3_000;
22
+ /** !Cleanup Phase 7 Slice 1 (I21, I24) — HOW MANY AGENTS MAY RUN AT ONCE.
23
+ *
24
+ * Was structurally ONE (a boolean `busy` on the listener state), which is the
25
+ * defect behind the user's first requirement: a second request typed seconds
26
+ * after the first sat untouched until the first finished, for up to the whole
27
+ * spawn timeout.
28
+ *
29
+ * THREE, and bounded on purpose. Each run is a real headless `claude` process
30
+ * with real memory and real API cost, so a burst of twenty requests must not
31
+ * fork twenty agents. Three is enough that a question is never stuck behind a
32
+ * build — the case the user named — while staying comfortable on a laptop that
33
+ * is also running the user's editor and browser.
34
+ *
35
+ * Overridable per machine, because the right number on a 64 GB desktop is not
36
+ * the right number on a laptop.
37
+ *
38
+ * THE REAL CEILING TURNED OUT NOT TO BE THE MACHINE. The Phase 7 Slice 1 walk
39
+ * ran three concurrent Opus workers and two of them came back `claude exited 1`
40
+ * while the third answered normally — the agent binary was fine when run by
41
+ * hand, and the stream carried a `rate_limit_event` for the five-hour window.
42
+ * So the binding constraint on concurrency is the user's API rate limit, not
43
+ * RAM or CPU, and raising this number past what the account allows converts
44
+ * work that would have queued into work that FAILS and burns attempts. A
45
+ * rate-limited run is currently indistinguishable from a crashed one, which is
46
+ * worth fixing before this default goes any higher. */
47
+ export const ORCHESTRATOR_MAX_CONCURRENT = Number(process.env.CTRL_SPC_V2_MAX_CONCURRENT) || 3;
48
+ /** Hard ceiling on one headless agent run before it is killed and the todo is
49
+ * released for retry. Generous on purpose: the spawned agent does REAL WORK
50
+ * (ux.md § Purpose) — it may cut a worktree, run a build, read a codebase — so a
51
+ * short timeout would kill legitimate work and hand the user a released todo
52
+ * that keeps failing the same way. It exists only so a wedged process cannot
53
+ * hold the single in-flight slot forever. */
54
+ export const ORCHESTRATOR_SPAWN_TIMEOUT_MS = 30 * 60_000;
13
55
  /** How long a work session stays "working" after the agent's last ctrl-spc tool
14
56
  * call, with no further activity. The companion keeps last_seen_at fresh within
15
57
  * this window; once it lapses (agent finished, Ctrl-C'd, crashed, or asleep) the
@@ -0,0 +1,98 @@
1
+ /**
2
+ * 18a SLICE 5: WHAT THE USER READS WHEN AN ATTEMPT FAILS.
3
+ *
4
+ * .implementations/18-orchestrator/18a-you-answer-and-the-work-continues/ux.md
5
+ * § "Slice 5. A failure you did not cause reads like the machine"
6
+ *
7
+ * The daemon's internal error strings are exit statuses: `claude exited 1`,
8
+ * `claude exited ENOENT`, `codex produced no output`. They are the right thing
9
+ * to LOG, and they are what every existing test asserts on, so they are left
10
+ * exactly as they are. What was wrong is that the same string reached the card.
11
+ *
12
+ * An exit code is not an explanation. It is identical whether the binary is
13
+ * missing, the account is rate limited, or the agent genuinely crashed, and the
14
+ * user cannot act on any of them from the number. This translates at the point
15
+ * the reason is STORED, so the log keeps the diagnosis and the card gets the
16
+ * sentence.
17
+ *
18
+ * THE FALLBACK IS HONEST, not a guess. An error this does not recognise becomes
19
+ * "the agent stopped without finishing", which is true of every one of them, and
20
+ * the technical string is still in the daemon log for whoever needs it. Inventing
21
+ * a cause from an exit code would be the same defect wearing better words.
22
+ */
23
+ /** How many times the orchestrator dispatches before giving up.
24
+ * `MAX_ATTEMPTS` in orchestrator.ts is the definition; this module does not
25
+ * need it, but the web app's copy of the number is documented against the same
26
+ * fact: it is deliberately not configurable, so a constant cannot drift into a
27
+ * lie the way a configurable one would. */
28
+ /**
29
+ * The agent's name as the user picked it in the machine popover.
30
+ *
31
+ * Shared rather than inlined because 18a Slice 7 composes its own sentence: the
32
+ * liveness watch does not have an error string to translate, it has a FACT (the
33
+ * process is gone), and a sentence written from that fact is more honest than
34
+ * one matched out of a string nobody wrote.
35
+ *
36
+ * An agent this does not know about is passed through rather than mangled.
37
+ */
38
+ export function agentDisplayName(agent) {
39
+ return agent === 'claude' ? 'Claude' : agent === 'codex' ? 'Codex' : agent;
40
+ }
41
+ /**
42
+ * Plain words for a failed attempt, from the daemon's own error string plus
43
+ * whatever the child printed to stderr.
44
+ *
45
+ * `agent` is capitalised into the sentence because the user picked it by name
46
+ * ("Claude", "Codex") and a sentence about "the agent" reads as being about
47
+ * something they did not choose.
48
+ */
49
+ export function plainFailureReason(agent, error, stderr = '') {
50
+ const name = agentDisplayName(agent);
51
+ const haystack = `${error}\n${stderr}`.toLowerCase();
52
+ /* THE COMMAND IS NOT THERE. `ENOENT` from spawn is the fixture the scenario
53
+ uses (the agent removed from PATH) and the single most likely real cause on
54
+ a fresh machine. It is also the one the user can fix in a minute, which is
55
+ why it is worth naming rather than folding into the fallback. */
56
+ if (haystack.includes('enoent') || haystack.includes('command not found')) {
57
+ return `${name} is not installed on this machine, or is not on its PATH.`;
58
+ }
59
+ /* RATE LIMITED. ux.md records the measurement this came from: on 2026-08-07
60
+ two of three concurrent runs died on the account's rate limit while the same
61
+ work succeeded alone. It is the reason the cap stays at three, and until now
62
+ it was indistinguishable from a crash. */
63
+ if (haystack.includes('rate limit') || haystack.includes('rate_limit')
64
+ || haystack.includes('429') || haystack.includes('overloaded')) {
65
+ return `${name}'s usage limit is in effect on this account, so the run could not start.`;
66
+ }
67
+ /* SIGNED OUT. Recoverable by the user, and silent otherwise: the run just
68
+ fails, over and over, for as long as the session is stale.
69
+
70
+ 18a SLICE 7 ADDED THE OAUTH AND 401 SIGNATURES, from the real thing rather
71
+ than from imagination. The Windows machine's Claude Code had a stale token
72
+ and said, on STDOUT as a stream-json result: `"error":
73
+ "authentication_failed"` and `API Error: 401 OAuth access token has
74
+ expired.` The word "authentication" above would have matched that, but only
75
+ once stdout was being read at all, which is the other half of the fix. `401`
76
+ and `oauth` are here so the same failure is caught when it arrives in
77
+ shorter words. */
78
+ if (haystack.includes('unauthorized') || haystack.includes('not logged in')
79
+ || haystack.includes('authentication') || haystack.includes('oauth')
80
+ || haystack.includes('401')) {
81
+ return `${name} is not signed in on this machine.`;
82
+ }
83
+ /* THE RUN WAS KILLED, by the timeout or by a signal from outside. Not the
84
+ user's Stop, which never reaches here: a stopped run is settled with its own
85
+ flag well before this. */
86
+ if (haystack.includes('timed out')) {
87
+ return `The run was stopped for taking too long.`;
88
+ }
89
+ if (haystack.includes('killed by')) {
90
+ return `${name} was killed on this machine before it finished.`;
91
+ }
92
+ /* IT RAN AND SAID NOTHING. A clean exit with no answer, which is a real state
93
+ and reads very differently from a crash. */
94
+ if (haystack.includes('produced no output')) {
95
+ return `${name} finished without producing an answer.`;
96
+ }
97
+ return `${name} stopped without finishing.`;
98
+ }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ const HELP = `cs — CTRL+SPC
11
11
  cs Open the Companion app (the front door)
12
12
  cs open Open the Companion app in your browser
13
13
  cs login Sign in from the terminal and link this computer
14
- cs start Come online now, no window (used by auto-start)
14
+ cs start Come online and answer cards, no window (used by auto-start)
15
15
  cs status Show sign-in state, computer, and detected agents
16
16
  cs autostart on Come online automatically at login
17
17
  cs autostart off Stop coming online at login