@cluesmith/codev 3.3.2 → 3.3.4
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/dashboard-dist/assets/{index-D3XaKpZW.js → index-DZf6pNPE.js} +41 -41
- package/dashboard-dist/assets/index-DZf6pNPE.js.map +1 -0
- package/dashboard-dist/assets/index-DnYLy5C_.css +32 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +48 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/ide.d.ts +20 -0
- package/dist/agent-farm/commands/ide.d.ts.map +1 -0
- package/dist/agent-farm/commands/ide.js +138 -0
- package/dist/agent-farm/commands/ide.js.map +1 -0
- package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
- package/dist/agent-farm/commands/inbox.js +79 -10
- package/dist/agent-farm/commands/inbox.js.map +1 -1
- package/dist/agent-farm/commands/interrupt.d.ts +4 -1
- package/dist/agent-farm/commands/interrupt.d.ts.map +1 -1
- package/dist/agent-farm/commands/interrupt.js +6 -3
- package/dist/agent-farm/commands/interrupt.js.map +1 -1
- package/dist/agent-farm/commands/reset.d.ts +4 -1
- package/dist/agent-farm/commands/reset.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset.js +6 -3
- package/dist/agent-farm/commands/reset.js.map +1 -1
- package/dist/agent-farm/commands/send.d.ts +28 -0
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +79 -6
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +16 -7
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/tower.d.ts +21 -9
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +90 -30
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/db/index.d.ts +3 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +8 -443
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/mailbox.d.ts +47 -7
- package/dist/agent-farm/db/mailbox.d.ts.map +1 -1
- package/dist/agent-farm/db/mailbox.js +56 -12
- package/dist/agent-farm/db/mailbox.js.map +1 -1
- package/dist/agent-farm/db/migrations.d.ts +57 -0
- package/dist/agent-farm/db/migrations.d.ts.map +1 -0
- package/dist/agent-farm/db/migrations.js +530 -0
- package/dist/agent-farm/db/migrations.js.map +1 -0
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +1 -0
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/tower-owner.d.ts +168 -0
- package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
- package/dist/agent-farm/db/tower-owner.js +369 -0
- package/dist/agent-farm/db/tower-owner.js.map +1 -0
- package/dist/agent-farm/db/types.d.ts +47 -0
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/lib/ide-record.d.ts +65 -0
- package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
- package/dist/agent-farm/lib/ide-record.js +118 -0
- package/dist/agent-farm/lib/ide-record.js.map +1 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +96 -19
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/cron-delivery.d.ts +7 -1
- package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/cron-delivery.js +2 -1
- package/dist/agent-farm/servers/cron-delivery.js.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.d.ts +44 -2
- package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.js +79 -6
- package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
- package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
- package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-forward.js +274 -0
- package/dist/agent-farm/servers/ide-forward.js.map +1 -0
- package/dist/agent-farm/servers/ide-server.d.ts +68 -0
- package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
- package/dist/agent-farm/servers/ide-server.js +281 -0
- package/dist/agent-farm/servers/ide-server.js.map +1 -0
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +296 -12
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +566 -69
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -19
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +204 -37
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +138 -11
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +144 -35
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts +28 -12
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +130 -79
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +77 -1
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +174 -25
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +7 -4
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +7 -4
- package/dist/agent-farm/servers/session-submit.js.map +1 -1
- package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-cron.js +7 -3
- package/dist/agent-farm/servers/tower-cron.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +17 -10
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts +32 -0
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +186 -26
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +79 -9
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +21 -0
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +106 -13
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +35 -0
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +39 -0
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +26 -2
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +78 -2
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +107 -11
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/port.d.ts +22 -0
- package/dist/agent-farm/utils/port.d.ts.map +1 -0
- package/dist/agent-farm/utils/port.js +35 -0
- package/dist/agent-farm/utils/port.js.map +1 -0
- package/dist/agent-farm/utils/server-utils.d.ts +26 -0
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +36 -0
- package/dist/agent-farm/utils/server-utils.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +61 -1
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/tree-tripwire.d.ts +177 -0
- package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
- package/dist/commands/consult/tree-tripwire.js +356 -0
- package/dist/commands/consult/tree-tripwire.js.map +1 -0
- package/dist/commands/doctor.d.ts +69 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +165 -2
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/generate-image.d.ts +25 -1
- package/dist/commands/generate-image.d.ts.map +1 -1
- package/dist/commands/generate-image.js +410 -2
- package/dist/commands/generate-image.js.map +1 -1
- package/dist/commands/porch/prompts.d.ts.map +1 -1
- package/dist/commands/porch/prompts.js +18 -2
- package/dist/commands/porch/prompts.js.map +1 -1
- package/dist/lib/agent-env.d.ts +86 -0
- package/dist/lib/agent-env.d.ts.map +1 -0
- package/dist/lib/agent-env.js +126 -0
- package/dist/lib/agent-env.js.map +1 -0
- package/dist/lib/forge-contracts.d.ts +76 -5
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/forge-rate-limit.d.ts +45 -0
- package/dist/lib/forge-rate-limit.d.ts.map +1 -0
- package/dist/lib/forge-rate-limit.js +89 -0
- package/dist/lib/forge-rate-limit.js.map +1 -0
- package/dist/lib/forge.d.ts +29 -1
- package/dist/lib/forge.d.ts.map +1 -1
- package/dist/lib/forge.js +96 -21
- package/dist/lib/forge.js.map +1 -1
- package/dist/lib/github.d.ts +13 -1
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +58 -15
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/test-env.d.ts +17 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +20 -0
- package/dist/lib/test-env.js.map +1 -1
- package/dist/terminal/pty-manager.d.ts +10 -1
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +38 -9
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +174 -8
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +299 -25
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +23 -2
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +115 -36
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/session-screen.d.ts +34 -5
- package/dist/terminal/session-screen.d.ts.map +1 -1
- package/dist/terminal/session-screen.js +52 -6
- package/dist/terminal/session-screen.js.map +1 -1
- package/dist/terminal/shellper-client.d.ts +73 -0
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +151 -0
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-main.js +20 -3
- package/dist/terminal/shellper-main.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts +2 -0
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +17 -1
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +28 -0
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/dist/terminal/terminal-replies.d.ts +102 -0
- package/dist/terminal/terminal-replies.d.ts.map +1 -0
- package/dist/terminal/terminal-replies.js +181 -0
- package/dist/terminal/terminal-replies.js.map +1 -0
- package/package.json +4 -4
- package/scripts/forge/gitea/_lib.sh +214 -0
- package/scripts/forge/gitea/issue-comment.sh +8 -1
- package/scripts/forge/gitea/issue-view.sh +95 -4
- package/scripts/forge/gitea/pr-create.sh +155 -0
- package/scripts/forge/gitea/pr-exists.sh +32 -4
- package/scripts/forge/gitea/pr-list.sh +40 -31
- package/scripts/forge/gitea/pr-view.sh +65 -4
- package/scripts/forge/gitea/recently-merged.sh +91 -21
- package/scripts/forge/gitea/user-identity.sh +30 -1
- package/scripts/forge/github/issue-view.sh +5 -2
- package/scripts/forge/github/pr-create.sh +47 -0
- package/scripts/forge/github/pr-list.sh +6 -2
- package/scripts/forge/github/pr-search.sh +6 -2
- package/scripts/forge/gitlab/issue-search.sh +1 -0
- package/scripts/forge/gitlab/issue-view.sh +24 -4
- package/scripts/forge/gitlab/pr-create.sh +53 -0
- package/scripts/forge/gitlab/pr-search.sh +37 -2
- package/scripts/forge/linear/auth-status.sh +1 -0
- package/scripts/forge/linear/issue-comment.sh +1 -0
- package/scripts/forge/linear/issue-list.sh +1 -0
- package/scripts/forge/linear/issue-search.sh +1 -0
- package/scripts/forge/linear/issue-view.sh +14 -2
- package/scripts/forge/linear/recently-closed.sh +1 -0
- package/scripts/forge/linear/user-identity.sh +1 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +39 -1
- package/skeleton/.codex/skills/generate-image/SKILL.md +39 -1
- package/skeleton/protocols/air/prompts/pr.md +6 -1
- package/skeleton/protocols/aspir/prompts/review.md +6 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
- package/skeleton/protocols/bugfix/protocol.md +2 -1
- package/skeleton/protocols/maintain/prompts/review.md +6 -1
- package/skeleton/protocols/pir/prompts/review.md +10 -7
- package/skeleton/protocols/spir/prompts/review.md +6 -1
- package/skeleton/resources/commands/agent-farm.md +28 -3
- package/skeleton/roles/consultant.md +32 -0
- package/skeleton/templates/AGENTS.md +1 -1
- package/skeleton/templates/CLAUDE.md +1 -1
- package/templates/tower.html +16 -1
- package/dashboard-dist/assets/index-BZRTEMtH.css +0 -32
- package/dashboard-dist/assets/index-D3XaKpZW.js.map +0 -1
|
@@ -1,17 +1,113 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Paced message writing for PTY sessions (Bugfix #584).
|
|
2
|
+
* Paced message writing for PTY sessions (Bugfix #584, Issue #1567).
|
|
3
3
|
*
|
|
4
4
|
* Extracted to a shared module to avoid circular imports between
|
|
5
5
|
* tower-routes.ts and tower-cron.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { trySubmitToSession, watchBypasses } from './session-submit.js';
|
|
8
|
-
// Messages longer than this threshold are written line-by-line with delays
|
|
9
|
-
// to prevent the receiving terminal from classifying the input as a paste
|
|
10
|
-
// and swallowing the final Enter.
|
|
11
|
-
const PACED_WRITE_LINE_THRESHOLD = 4;
|
|
12
|
-
const INTER_LINE_DELAY_MS = 10;
|
|
13
|
-
const PACED_ENTER_DELAY_MS = 80;
|
|
14
8
|
const SIMPLE_ENTER_DELAY_MS = 50;
|
|
9
|
+
/**
|
|
10
|
+
* Issue #1567 — why long frames are no longer handed to the PTY in one write.
|
|
11
|
+
*
|
|
12
|
+
* The kernel splits a large master-side write at the slave's input-queue high-water mark
|
|
13
|
+
* (TTYHOG − 2 = 1022 bytes on macOS). The receiving TUI reads the first ~1 KB as one chunk,
|
|
14
|
+
* its paste heuristic classifies that chunk as a paste and DISCARDS it, then types the
|
|
15
|
+
* remainder and the Enter normally — so the agent saw a mid-sentence tail-only fragment
|
|
16
|
+
* while every `write()` reported success. Measured on claude 2.1.263 (harness in
|
|
17
|
+
* `scripts/bugfix-1567-head-loss-harness.mts`, evidence in `codev/evidence/1567-head-loss/`):
|
|
18
|
+
* a 1,172-byte 3-line frame lost its head 14/20 times on an IDLE composer; a 1,000-byte frame
|
|
19
|
+
* never did. Two independent changes each measured 0 losses, and both are applied:
|
|
20
|
+
*
|
|
21
|
+
* 1. **No single write exceeds {@link PASTE_CHUNK_BYTES}** — comfortably under the queue
|
|
22
|
+
* limit, so the kernel never splits a read and the reader drains between writes.
|
|
23
|
+
* 2. **Long frames travel as ONE explicit bracketed paste** (`ESC[200~` … `ESC[201~`), so the
|
|
24
|
+
* TUI's paste heuristic never runs on chunk boundaries and newlines inside the body can
|
|
25
|
+
* never be read as Enter. The Enter is a separate write OUTSIDE the bracket, after a
|
|
26
|
+
* settle. Inside the bracket newlines travel as `\r`, which is what a terminal emulator
|
|
27
|
+
* emits on paste (xterm.js) and what claude's composer expects there — the same convention
|
|
28
|
+
* `apps/vscode/src/review-queue/queue.ts` already uses to type a review into a builder.
|
|
29
|
+
*
|
|
30
|
+
* This replaces the #584-era per-line pacing for frames of {@link BRACKET_MIN_LINES} lines or
|
|
31
|
+
* more, which was tuned against an older TUI's heuristic. Short frames keep the pre-existing
|
|
32
|
+
* single write + delayed Enter, byte for byte, so `--raw` slash commands and short sends are
|
|
33
|
+
* untouched.
|
|
34
|
+
*/
|
|
35
|
+
export const PASTE_CHUNK_BYTES = 512;
|
|
36
|
+
/** Gap between consecutive chunk writes; the reader drains a chunk in well under this. */
|
|
37
|
+
export const PASTE_CHUNK_GAP_MS = 5;
|
|
38
|
+
/** Settle between the paste's closing marker and the Enter that submits it (measured: 0/29 losses). */
|
|
39
|
+
export const PASTE_ENTER_DELAY_MS = 80;
|
|
40
|
+
/**
|
|
41
|
+
* Frames LONGER than this many bytes, or of {@link BRACKET_MIN_LINES} lines or more, are
|
|
42
|
+
* bracketed and chunked. Well under the 1,022-byte queue limit where the loss starts: the
|
|
43
|
+
* field's smallest confirmed truncations were ~500-byte bodies, and a bracketed short paste
|
|
44
|
+
* costs nothing on the measured harnesses.
|
|
45
|
+
*/
|
|
46
|
+
export const BRACKET_MIN_BYTES = 256;
|
|
47
|
+
/** The #584 threshold, kept: any frame of this many lines or more is never typed line by line. */
|
|
48
|
+
export const BRACKET_MIN_LINES = 4;
|
|
49
|
+
/** Gap between line pieces on the non-bracketed (opt-out) strategy — the #584 pacing. */
|
|
50
|
+
const INTER_LINE_DELAY_MS = 10;
|
|
51
|
+
export const PASTE_BEGIN = '\x1b[200~';
|
|
52
|
+
export const PASTE_END = '\x1b[201~';
|
|
53
|
+
export const BRACKETED_PASTE = { bracketedPaste: true };
|
|
54
|
+
export const PLAIN_CHUNKED = { bracketedPaste: false };
|
|
55
|
+
/**
|
|
56
|
+
* The write strategy for a gate profile's `app`. agy is the one modeled harness whose paste
|
|
57
|
+
* handling was not measurable (unauthenticated), so it keeps the per-line shape; anything
|
|
58
|
+
* else is bracketed. Unknown apps never reach the write edge (the gate holds `no-profile`).
|
|
59
|
+
*/
|
|
60
|
+
export function writeStrategyForApp(app) {
|
|
61
|
+
return app === 'agy' ? PLAIN_CHUNKED : BRACKETED_PASTE;
|
|
62
|
+
}
|
|
63
|
+
/** Does this frame take the bracketed/chunked path rather than the single short write? */
|
|
64
|
+
export function isLongFrame(message) {
|
|
65
|
+
return message.split('\n').length >= BRACKET_MIN_LINES || Buffer.byteLength(message, 'utf8') > BRACKET_MIN_BYTES;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Split `text` into pieces of at most `max` bytes, never inside a UTF-8 sequence (a split
|
|
69
|
+
* continuation byte would reach the TUI as two invalid characters).
|
|
70
|
+
*/
|
|
71
|
+
export function chunkForPty(text, max = PASTE_CHUNK_BYTES) {
|
|
72
|
+
const buf = Buffer.from(text, 'utf8');
|
|
73
|
+
const out = [];
|
|
74
|
+
for (let at = 0; at < buf.length;) {
|
|
75
|
+
let end = Math.min(buf.length, at + max);
|
|
76
|
+
while (end < buf.length && end > at && (buf[end] & 0xc0) === 0x80)
|
|
77
|
+
end--;
|
|
78
|
+
out.push(buf.subarray(at, end).toString('utf8'));
|
|
79
|
+
at = end;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The pieces of a long frame, in write order, for a strategy. Bracketed: the body (newlines
|
|
85
|
+
* as `\r`) chunked, with the opening marker on the first piece and the closing marker on the
|
|
86
|
+
* last — the markers are never split across writes, so no chunk boundary can land inside an
|
|
87
|
+
* escape sequence, and every piece INCLUDING its marker stays within {@link PASTE_CHUNK_BYTES}.
|
|
88
|
+
* Plain: one piece per line (`\n` kept, as #584 wrote it), with any line over
|
|
89
|
+
* the chunk size split further.
|
|
90
|
+
*/
|
|
91
|
+
export function framePieces(message, strategy) {
|
|
92
|
+
if (strategy.bracketedPaste) {
|
|
93
|
+
// A literal paste marker inside the body (a pasted terminal log, say) would end or restart
|
|
94
|
+
// the paste mid-frame and let the remainder be typed as keys; strip both so the bracket
|
|
95
|
+
// we add is the only one the TUI sees.
|
|
96
|
+
const body = message.replace(/\r?\n/g, '\r').split(PASTE_BEGIN).join('').split(PASTE_END).join('');
|
|
97
|
+
// Chunk with room for the markers so NO write — first or last piece included — exceeds
|
|
98
|
+
// PASTE_CHUNK_BYTES on the wire.
|
|
99
|
+
const pieces = chunkForPty(body, PASTE_CHUNK_BYTES - PASTE_BEGIN.length - PASTE_END.length);
|
|
100
|
+
// A body that was NOTHING but markers is empty after stripping; still emit one (empty) paste
|
|
101
|
+
// so the frame is a well-formed no-op rather than the literal text "undefined".
|
|
102
|
+
if (pieces.length === 0)
|
|
103
|
+
pieces.push('');
|
|
104
|
+
pieces[0] = PASTE_BEGIN + pieces[0];
|
|
105
|
+
pieces[pieces.length - 1] += PASTE_END;
|
|
106
|
+
return pieces;
|
|
107
|
+
}
|
|
108
|
+
const lines = message.split('\n');
|
|
109
|
+
return lines.flatMap((line, i) => chunkForPty(i < lines.length - 1 ? line + '\n' : line));
|
|
110
|
+
}
|
|
15
111
|
/** ESC keystroke — ends the agent's current turn (Spec 1273). */
|
|
16
112
|
export const ESC = '\x1b';
|
|
17
113
|
/**
|
|
@@ -43,21 +139,22 @@ export function writeEscapeToSession(session, noEnter) {
|
|
|
43
139
|
return ESCAPE_ENTER_DELAY_MS;
|
|
44
140
|
}
|
|
45
141
|
/**
|
|
46
|
-
* Write a message to a PTY session
|
|
47
|
-
*
|
|
142
|
+
* Write a message to a PTY session (Bugfix #584, Issue #1567).
|
|
143
|
+
*
|
|
144
|
+
* Short frames (under {@link BRACKET_MIN_LINES} lines AND at most {@link BRACKET_MIN_BYTES}
|
|
145
|
+
* bytes): a single write, then Enter after 50 ms — unchanged since #584.
|
|
48
146
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* after
|
|
147
|
+
* Long frames: {@link framePieces} written {@link PASTE_CHUNK_GAP_MS} apart (bracketed by
|
|
148
|
+
* default; see {@link WriteStrategy}), then Enter as its own write
|
|
149
|
+
* {@link PASTE_ENTER_DELAY_MS} after the last piece. The Enter is always outside the paste
|
|
150
|
+
* bracket, so a TUI that collapses the paste into a placard still submits it.
|
|
52
151
|
*
|
|
53
152
|
* @param delayOffset ms offset for all scheduled writes (used to serialize
|
|
54
153
|
* multiple messages to the same session without interleaving)
|
|
55
154
|
* @returns ms timestamp (from call time) when all writes complete
|
|
56
155
|
*/
|
|
57
|
-
export function writeMessageToSession(session, message, noEnter, delayOffset = 0) {
|
|
58
|
-
|
|
59
|
-
if (lines.length < PACED_WRITE_LINE_THRESHOLD) {
|
|
60
|
-
// Short messages: single write (existing behavior, works fine)
|
|
156
|
+
export function writeMessageToSession(session, message, noEnter, delayOffset = 0, strategy = BRACKETED_PASTE) {
|
|
157
|
+
if (!isLongFrame(message)) {
|
|
61
158
|
if (delayOffset === 0) {
|
|
62
159
|
session.write(message);
|
|
63
160
|
}
|
|
@@ -70,26 +167,23 @@ export function writeMessageToSession(session, message, noEnter, delayOffset = 0
|
|
|
70
167
|
}
|
|
71
168
|
return enterTime;
|
|
72
169
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (lineDelay === 0) {
|
|
80
|
-
session.write(text);
|
|
170
|
+
const pieces = framePieces(message, strategy);
|
|
171
|
+
const gap = strategy.bracketedPaste ? PASTE_CHUNK_GAP_MS : INTER_LINE_DELAY_MS;
|
|
172
|
+
for (let i = 0; i < pieces.length; i++) {
|
|
173
|
+
const at = delayOffset + i * gap;
|
|
174
|
+
if (at === 0) {
|
|
175
|
+
session.write(pieces[i]);
|
|
81
176
|
}
|
|
82
177
|
else {
|
|
83
|
-
setTimeout(() => session.write(
|
|
178
|
+
setTimeout(() => session.write(pieces[i]), at);
|
|
84
179
|
}
|
|
85
180
|
}
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return lastLineTime;
|
|
181
|
+
const lastPieceTime = delayOffset + (pieces.length - 1) * gap;
|
|
182
|
+
if (noEnter)
|
|
183
|
+
return lastPieceTime;
|
|
184
|
+
const enterTime = lastPieceTime + PASTE_ENTER_DELAY_MS;
|
|
185
|
+
setTimeout(() => session.write('\r'), enterTime);
|
|
186
|
+
return enterTime;
|
|
93
187
|
}
|
|
94
188
|
/**
|
|
95
189
|
* Paced write of a message (text + trailing Enter unless `noEnter`) performed as ONE
|
|
@@ -117,7 +211,7 @@ export function writeMessageToSession(session, message, noEnter, delayOffset = 0
|
|
|
117
211
|
* `submitToSession` schedules its own equal-offset sleep, so the Enter still executes
|
|
118
212
|
* first — which is what makes the per-agent serializer's completion-chaining real.
|
|
119
213
|
*/
|
|
120
|
-
export async function submitMessagePaced(session, message, noEnter, precheck, clock) {
|
|
214
|
+
export async function submitMessagePaced(session, message, noEnter, precheck, clock, strategy = BRACKETED_PASTE) {
|
|
121
215
|
// Fail LOUD on a missing id rather than keying the lock on `undefined`. Sessions reach
|
|
122
216
|
// this through structurally-typed ports, so a double without an id compiles fine and
|
|
123
217
|
// would silently collapse every per-terminal lock into one global lock — serialization
|
|
@@ -137,9 +231,17 @@ export async function submitMessagePaced(session, message, noEnter, precheck, cl
|
|
|
137
231
|
try {
|
|
138
232
|
let delivered = true;
|
|
139
233
|
let abort = null;
|
|
234
|
+
// Sampled inside the lock, immediately before the first byte (below), so the comparison
|
|
235
|
+
// spans exactly the paced write and not the lock wait that preceded it.
|
|
236
|
+
let inputSeqBefore = session.inputSeq;
|
|
237
|
+
// The origin is HARD-CODED here rather than forwarded through a parameter (Issue #1473).
|
|
238
|
+
// A one-arg function is assignable to a two-arg function type, so a wrapper that forgot to
|
|
239
|
+
// pass the origin along would compile cleanly — and its failure mode is the delivery
|
|
240
|
+
// counting its own bytes as human input, i.e. "mail never delivers". Removing the parameter
|
|
241
|
+
// removes the opportunity.
|
|
140
242
|
const tracked = {
|
|
141
243
|
write: (data) => {
|
|
142
|
-
const ok = session.write(data);
|
|
244
|
+
const ok = session.write(data, 'delivery');
|
|
143
245
|
if (!ok)
|
|
144
246
|
delivered = false;
|
|
145
247
|
return ok;
|
|
@@ -149,7 +251,8 @@ export async function submitMessagePaced(session, message, noEnter, precheck, cl
|
|
|
149
251
|
abort = precheck();
|
|
150
252
|
if (abort !== null)
|
|
151
253
|
return 0; // refused in-lock: not one byte goes out
|
|
152
|
-
|
|
254
|
+
inputSeqBefore = session.inputSeq;
|
|
255
|
+
return writeMessageToSession(tracked, message, noEnter, 0, strategy);
|
|
153
256
|
}, clock);
|
|
154
257
|
if (!ran)
|
|
155
258
|
return { status: 'contended' };
|
|
@@ -162,6 +265,12 @@ export async function submitMessagePaced(session, message, noEnter, precheck, cl
|
|
|
162
265
|
return { status: 'dropped' };
|
|
163
266
|
if (bypasses.raced())
|
|
164
267
|
return { status: 'preempted' };
|
|
268
|
+
// The prechecks close everything up to the first byte; between the first byte and the
|
|
269
|
+
// trailing Enter there is still an 80 ms-to-seconds window, and by then the bytes are out.
|
|
270
|
+
// So this is a REPORTING signal — flag it and let the caller tell the operator, rather than
|
|
271
|
+
// hold and re-write a message that may well have landed (#1584). Omitted when false.
|
|
272
|
+
if (session.inputSeq !== inputSeqBefore)
|
|
273
|
+
return { status: 'written', racedByInput: true };
|
|
165
274
|
return { status: 'written' };
|
|
166
275
|
}
|
|
167
276
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-write.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/message-write.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAoB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"message-write.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/message-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* mode when the `gh` CLI is unavailable.
|
|
8
8
|
*/
|
|
9
9
|
import type { ForgeIssueListItem } from '../../lib/github.js';
|
|
10
|
+
import { type RateLimitProbe } from '../../lib/forge-rate-limit.js';
|
|
10
11
|
import type { PlanPhase, OverviewBuilder, OverviewBacklogItem, OverviewData } from '@cluesmith/codev-types';
|
|
11
12
|
interface ParsedStatus {
|
|
12
13
|
id: string;
|
|
@@ -161,15 +162,28 @@ export declare function readFeedbackMode(workspaceRoot: string): 'forward' | 'qu
|
|
|
161
162
|
* Derive backlog from open GitHub issues cross-referenced with specs and builders.
|
|
162
163
|
*/
|
|
163
164
|
export declare function deriveBacklog(issues: ForgeIssueListItem[], workspaceRoot: string, activeBuilderIssues: Set<string>, prLinkedIssues: Set<string>): OverviewBacklogItem[];
|
|
165
|
+
export interface OverviewCacheOptions {
|
|
166
|
+
/** Clock, injectable for tests. */
|
|
167
|
+
now?: () => number;
|
|
168
|
+
/** Reset probe run once per fresh suspension (Tower passes the gh probe). */
|
|
169
|
+
probe?: RateLimitProbe;
|
|
170
|
+
}
|
|
164
171
|
export declare class OverviewCache {
|
|
165
172
|
private prCache;
|
|
166
173
|
private issueCache;
|
|
167
174
|
private closedCache;
|
|
168
175
|
private mergedPRCache;
|
|
169
176
|
private currentUserCache;
|
|
177
|
+
/** Single-flight per `<workspace>:<concept>` (#1645). */
|
|
178
|
+
private inflight;
|
|
179
|
+
/** Escalating failure window per `<workspace>:<budget>`. */
|
|
180
|
+
private failureWindows;
|
|
181
|
+
/** Epoch of the last invalidate(); open-list entries fetched before it are stale once debounced. */
|
|
182
|
+
private invalidatedAt;
|
|
183
|
+
private readonly limiter;
|
|
184
|
+
private readonly now;
|
|
185
|
+
constructor(options?: OverviewCacheOptions);
|
|
170
186
|
private resolvedEnrichment;
|
|
171
|
-
private readonly TTL;
|
|
172
|
-
private readonly USER_TTL;
|
|
173
187
|
/**
|
|
174
188
|
* Build the overview response. Aggregates builder state, PRs, and backlog.
|
|
175
189
|
*
|
|
@@ -180,20 +194,22 @@ export declare class OverviewCache {
|
|
|
180
194
|
*/
|
|
181
195
|
getOverview(workspaceRoot: string, activeBuilderRoleIds?: Set<string>): Promise<OverviewData>;
|
|
182
196
|
/**
|
|
183
|
-
*
|
|
197
|
+
* Mark the open lists (PRs, issues) stale. Porch, cleanup and the VS Code
|
|
198
|
+
* review queue call this after every mutation, so it is not a human signal
|
|
199
|
+
* (#1645): it never clears a suspension or a cached failure, never touches
|
|
200
|
+
* the search or identity caches, and a list is refetched for it at most once
|
|
201
|
+
* per `INVALIDATE_DEBOUNCE_MS` per workspace.
|
|
184
202
|
*/
|
|
185
203
|
invalidate(): void;
|
|
186
|
-
private fetchPRsCached;
|
|
187
|
-
private fetchIssuesCached;
|
|
188
204
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* Only successful resolutions are cached, so a transient failure (gh
|
|
192
|
-
* logged out, offline) self-heals on the next overview poll.
|
|
205
|
+
* Serve `concept` for `cwd` from cache, or dispatch exactly one fetch.
|
|
206
|
+
* `listCache` entries additionally honour invalidate() (debounced).
|
|
193
207
|
*/
|
|
194
|
-
private
|
|
195
|
-
|
|
196
|
-
private
|
|
208
|
+
private cachedFetch;
|
|
209
|
+
/** Run the concept once and record the outcome. Never rejects. */
|
|
210
|
+
private dispatch;
|
|
211
|
+
/** Current failure window for a key; escalates once per elapsed window, not per failure. */
|
|
212
|
+
private failureWindow;
|
|
197
213
|
}
|
|
198
214
|
export {};
|
|
199
215
|
//# sourceMappingURL=overview.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/overview.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"overview.d.ts","sourceRoot":"","sources":["../../../src/agent-farm/servers/overview.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { UNCATEGORIZED_AREA } from '@cluesmith/codev-sdk/constants';
|
|
12
|
-
import {
|
|
12
|
+
import { fetchForOverview, overviewBackend, parseLinkedIssue, parseLabelDefaults, parseArea, } from '../../lib/github.js';
|
|
13
|
+
import { ForgeRateLimiter, budgetKeyFor } from '../../lib/forge-rate-limit.js';
|
|
13
14
|
import { loadProtocol } from '../../commands/porch/protocol.js';
|
|
14
15
|
import { ResolvedEnrichmentCache } from './resolved-enrichment-cache.js';
|
|
15
16
|
import Database from 'better-sqlite3';
|
|
@@ -761,23 +762,41 @@ export function deriveBacklog(issues, workspaceRoot, activeBuilderIssues, prLink
|
|
|
761
762
|
return item;
|
|
762
763
|
});
|
|
763
764
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
765
|
+
/** Open lists: what porch/VS Code invalidation refreshes. */
|
|
766
|
+
const LIST_TTL_MS = 180_000;
|
|
767
|
+
/** The two 24 h searches: invalidation never touches these. */
|
|
768
|
+
const SEARCH_TTL_MS = 600_000;
|
|
769
|
+
/** GitHub identity is session-stable. */
|
|
770
|
+
const USER_TTL_MS = 3_600_000;
|
|
771
|
+
/** A list refetches at most once per minute per workspace because of invalidation. */
|
|
772
|
+
const INVALIDATE_DEBOUNCE_MS = 60_000;
|
|
773
|
+
/** Failure window: 60 s, doubling per window (not per failed command) to 15 min. */
|
|
774
|
+
const FAILURE_WINDOW_MIN_MS = 60_000;
|
|
775
|
+
const FAILURE_WINDOW_MAX_MS = 15 * 60_000;
|
|
767
776
|
export class OverviewCache {
|
|
768
777
|
prCache = new Map();
|
|
769
778
|
issueCache = new Map();
|
|
770
779
|
closedCache = new Map();
|
|
771
780
|
mergedPRCache = new Map();
|
|
772
781
|
currentUserCache = new Map();
|
|
782
|
+
/** Single-flight per `<workspace>:<concept>` (#1645). */
|
|
783
|
+
inflight = new Map();
|
|
784
|
+
/** Escalating failure window per `<workspace>:<budget>`. */
|
|
785
|
+
failureWindows = new Map();
|
|
786
|
+
/** Epoch of the last invalidate(); open-list entries fetched before it are stale once debounced. */
|
|
787
|
+
invalidatedAt = 0;
|
|
788
|
+
limiter;
|
|
789
|
+
now;
|
|
790
|
+
constructor(options = {}) {
|
|
791
|
+
this.now = options.now ?? (() => Date.now());
|
|
792
|
+
this.limiter = new ForgeRateLimiter(options.probe);
|
|
793
|
+
}
|
|
773
794
|
// Cache of issue-derived builder fields (today: `area`) keyed by worktreePath,
|
|
774
795
|
// so they survive refreshes where the source issue is unreachable rather than
|
|
775
796
|
// snapping back to their default while the builder is still listed (PIR #907).
|
|
776
797
|
// Intentionally NOT cleared by invalidate(): surviving cache invalidation is
|
|
777
798
|
// the whole point. See ResolvedEnrichmentCache.
|
|
778
799
|
resolvedEnrichment = new ResolvedEnrichmentCache();
|
|
779
|
-
TTL = 30_000;
|
|
780
|
-
USER_TTL = 3_600_000; // 1h — GitHub identity is session-stable
|
|
781
800
|
/**
|
|
782
801
|
* Build the overview response. Aggregates builder state, PRs, and backlog.
|
|
783
802
|
*
|
|
@@ -849,17 +868,29 @@ export class OverviewCache {
|
|
|
849
868
|
.filter((id) => id !== null));
|
|
850
869
|
// 2. Fetch PRs, issues, recently closed, merged PRs, and current user in
|
|
851
870
|
// parallel (each is independently cached)
|
|
852
|
-
const [
|
|
853
|
-
this.
|
|
854
|
-
this.
|
|
855
|
-
this.
|
|
856
|
-
this.
|
|
857
|
-
this.
|
|
871
|
+
const [prsOutcome, issuesOutcome, closedOutcome, mergedOutcome, userOutcome] = await Promise.all([
|
|
872
|
+
this.cachedFetch(this.prCache, workspaceRoot, 'pr-list', LIST_TTL_MS, true),
|
|
873
|
+
this.cachedFetch(this.issueCache, workspaceRoot, 'issue-list', LIST_TTL_MS, true),
|
|
874
|
+
this.cachedFetch(this.closedCache, workspaceRoot, 'recently-closed', SEARCH_TTL_MS, false),
|
|
875
|
+
this.cachedFetch(this.mergedPRCache, workspaceRoot, 'recently-merged', SEARCH_TTL_MS, false),
|
|
876
|
+
this.cachedFetch(this.currentUserCache, workspaceRoot, 'user-identity', USER_TTL_MS, false),
|
|
858
877
|
]);
|
|
878
|
+
const prs = prsOutcome.data;
|
|
879
|
+
const issues = issuesOutcome.data;
|
|
880
|
+
const closed = closedOutcome.data;
|
|
881
|
+
const mergedPRs = mergedOutcome.data;
|
|
882
|
+
const currentUser = userOutcome.data;
|
|
883
|
+
// Every forge-backed section counts, the two searches included (#1645).
|
|
884
|
+
const sections = [prsOutcome, issuesOutcome, closedOutcome, mergedOutcome];
|
|
885
|
+
const rateLimited = sections.find(o => o.rateLimitedUntil !== null);
|
|
886
|
+
const forgeStatus = rateLimited
|
|
887
|
+
? 'rate-limited'
|
|
888
|
+
: sections.some(o => o.data === null) ? 'unavailable' : 'ok';
|
|
889
|
+
const forgeResetAt = rateLimited ? new Date(rateLimited.rateLimitedUntil).toISOString() : undefined;
|
|
859
890
|
// 3. Process PRs
|
|
860
891
|
let pendingPRs = [];
|
|
861
892
|
if (prs === null) {
|
|
862
|
-
errors.prs = '
|
|
893
|
+
errors.prs = describeMissing(prsOutcome, 'PRs');
|
|
863
894
|
}
|
|
864
895
|
else {
|
|
865
896
|
pendingPRs = prs.map(pr => ({
|
|
@@ -885,7 +916,7 @@ export class OverviewCache {
|
|
|
885
916
|
? new Map(issues.map(i => [String(i.number), parseArea(i.labels)]))
|
|
886
917
|
: null;
|
|
887
918
|
if (issues === null) {
|
|
888
|
-
errors.issues = '
|
|
919
|
+
errors.issues = describeMissing(issuesOutcome, 'issues');
|
|
889
920
|
}
|
|
890
921
|
else {
|
|
891
922
|
backlog = deriveBacklog(issues, workspaceRoot, activeBuilderIssues, prLinkedIssues);
|
|
@@ -978,7 +1009,10 @@ export class OverviewCache {
|
|
|
978
1009
|
// has no view of the live terminal sessions. `handleOverview` (tower-routes.ts)
|
|
979
1010
|
// injects the real architect list via `liveArchitects` before serialization,
|
|
980
1011
|
// mirroring how it enriches `lastDataAt`.
|
|
981
|
-
const result = { builders, pendingPRs, backlog, recentlyClosed, architects: [], heldCount, mailboxEscalated, queuedFeedback, feedbackMode };
|
|
1012
|
+
const result = { builders, pendingPRs, backlog, recentlyClosed, architects: [], heldCount, mailboxEscalated, queuedFeedback, feedbackMode, forgeStatus };
|
|
1013
|
+
if (forgeResetAt) {
|
|
1014
|
+
result.forgeResetAt = forgeResetAt;
|
|
1015
|
+
}
|
|
982
1016
|
if (currentUser) {
|
|
983
1017
|
result.currentUser = currentUser;
|
|
984
1018
|
}
|
|
@@ -988,87 +1022,104 @@ export class OverviewCache {
|
|
|
988
1022
|
return result;
|
|
989
1023
|
}
|
|
990
1024
|
/**
|
|
991
|
-
*
|
|
1025
|
+
* Mark the open lists (PRs, issues) stale. Porch, cleanup and the VS Code
|
|
1026
|
+
* review queue call this after every mutation, so it is not a human signal
|
|
1027
|
+
* (#1645): it never clears a suspension or a cached failure, never touches
|
|
1028
|
+
* the search or identity caches, and a list is refetched for it at most once
|
|
1029
|
+
* per `INVALIDATE_DEBOUNCE_MS` per workspace.
|
|
992
1030
|
*/
|
|
993
1031
|
invalidate() {
|
|
994
|
-
this.
|
|
995
|
-
this.issueCache.clear();
|
|
996
|
-
this.closedCache.clear();
|
|
997
|
-
this.mergedPRCache.clear();
|
|
998
|
-
this.currentUserCache.clear();
|
|
1032
|
+
this.invalidatedAt = this.now();
|
|
999
1033
|
// Note: resolvedEnrichment is deliberately NOT cleared here. It must survive
|
|
1000
1034
|
// invalidation so a cleanup-triggered refresh (which calls invalidate()) can
|
|
1001
1035
|
// still fall back to a builder's resolved area instead of UNCATEGORIZED
|
|
1002
1036
|
// (PIR #907). It self-prunes in getOverview when a builder disappears.
|
|
1003
1037
|
}
|
|
1004
1038
|
// ===========================================================================
|
|
1005
|
-
// Private cache helpers
|
|
1039
|
+
// Private cache helpers (#1645)
|
|
1006
1040
|
// ===========================================================================
|
|
1007
|
-
async fetchPRsCached(cwd) {
|
|
1008
|
-
const now = Date.now();
|
|
1009
|
-
const cached = this.prCache.get(cwd);
|
|
1010
|
-
if (cached && (now - cached.fetchedAt) < this.TTL) {
|
|
1011
|
-
return cached.data;
|
|
1012
|
-
}
|
|
1013
|
-
const data = await fetchPRList(cwd);
|
|
1014
|
-
if (data !== null) {
|
|
1015
|
-
this.prCache.set(cwd, { data, fetchedAt: now });
|
|
1016
|
-
}
|
|
1017
|
-
return data;
|
|
1018
|
-
}
|
|
1019
|
-
async fetchIssuesCached(cwd) {
|
|
1020
|
-
const now = Date.now();
|
|
1021
|
-
const cached = this.issueCache.get(cwd);
|
|
1022
|
-
if (cached && (now - cached.fetchedAt) < this.TTL) {
|
|
1023
|
-
return cached.data;
|
|
1024
|
-
}
|
|
1025
|
-
const data = await fetchIssueList(cwd);
|
|
1026
|
-
if (data !== null) {
|
|
1027
|
-
this.issueCache.set(cwd, { data, fetchedAt: now });
|
|
1028
|
-
}
|
|
1029
|
-
return data;
|
|
1030
|
-
}
|
|
1031
1041
|
/**
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* Only successful resolutions are cached, so a transient failure (gh
|
|
1035
|
-
* logged out, offline) self-heals on the next overview poll.
|
|
1042
|
+
* Serve `concept` for `cwd` from cache, or dispatch exactly one fetch.
|
|
1043
|
+
* `listCache` entries additionally honour invalidate() (debounced).
|
|
1036
1044
|
*/
|
|
1037
|
-
|
|
1038
|
-
const now =
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1045
|
+
cachedFetch(cache, cwd, concept, ttl, listCache) {
|
|
1046
|
+
const now = this.now();
|
|
1047
|
+
const backend = overviewBackend(concept, cwd);
|
|
1048
|
+
const budget = backend === null ? null : budgetKeyFor(backend, concept);
|
|
1049
|
+
const entry = cache.get(cwd);
|
|
1050
|
+
const withinTtl = entry !== undefined && now < entry.expiresAt;
|
|
1051
|
+
// Only a *positive* entry goes stale on invalidation: a refresh means "my
|
|
1052
|
+
// data may be stale", never "the forge has recovered".
|
|
1053
|
+
const invalidated = listCache && entry !== undefined && entry.data !== null
|
|
1054
|
+
&& entry.fetchedAt < this.invalidatedAt && now - entry.fetchedAt >= INVALIDATE_DEBOUNCE_MS;
|
|
1055
|
+
const rateLimitedUntil = budget === null ? null : this.limiter.resetAt(budget, now);
|
|
1056
|
+
if (withinTtl && !invalidated) {
|
|
1057
|
+
return Promise.resolve({ data: entry.data, rateLimitedUntil, backend });
|
|
1046
1058
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const now = Date.now();
|
|
1051
|
-
const cached = this.closedCache.get(cwd);
|
|
1052
|
-
if (cached && (now - cached.fetchedAt) < this.TTL) {
|
|
1053
|
-
return cached.data;
|
|
1059
|
+
if (rateLimitedUntil !== null) {
|
|
1060
|
+
// Suspended: a list still inside its TTL is served stale; an expired one is empty.
|
|
1061
|
+
return Promise.resolve({ data: withinTtl ? entry.data : null, rateLimitedUntil, backend });
|
|
1054
1062
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
this.closedCache.set(cwd, { data, fetchedAt: now });
|
|
1063
|
+
if (backend === null || budget === null) {
|
|
1064
|
+
return Promise.resolve({ data: null, rateLimitedUntil: null, backend: null });
|
|
1058
1065
|
}
|
|
1059
|
-
|
|
1066
|
+
const key = `${cwd}:${concept}`;
|
|
1067
|
+
const existing = this.inflight.get(key);
|
|
1068
|
+
if (existing)
|
|
1069
|
+
return existing;
|
|
1070
|
+
const flight = this.dispatch(cache, cwd, concept, ttl, backend, budget).finally(() => {
|
|
1071
|
+
// Identity check: a settling flight must never unregister a newer one.
|
|
1072
|
+
if (this.inflight.get(key) === flight)
|
|
1073
|
+
this.inflight.delete(key);
|
|
1074
|
+
});
|
|
1075
|
+
this.inflight.set(key, flight);
|
|
1076
|
+
return flight;
|
|
1060
1077
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1078
|
+
/** Run the concept once and record the outcome. Never rejects. */
|
|
1079
|
+
async dispatch(cache, cwd, concept, ttl, backend, budget) {
|
|
1080
|
+
const dispatchedAt = this.now();
|
|
1081
|
+
let data = null;
|
|
1082
|
+
let errorText = null;
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await fetchForOverview(concept, cwd);
|
|
1085
|
+
data = result.data;
|
|
1086
|
+
if (result.error)
|
|
1087
|
+
errorText = `${result.error.stderr}\n${result.error.message}`;
|
|
1088
|
+
else if (data === null)
|
|
1089
|
+
errorText = '';
|
|
1066
1090
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
this.mergedPRCache.set(cwd, { data, fetchedAt: now });
|
|
1091
|
+
catch (err) {
|
|
1092
|
+
errorText = err instanceof Error ? err.message : String(err);
|
|
1070
1093
|
}
|
|
1071
|
-
|
|
1094
|
+
const now = this.now();
|
|
1095
|
+
if (errorText !== null) {
|
|
1096
|
+
const suspended = this.limiter.noteFailure(budget, backend, cwd, errorText, now);
|
|
1097
|
+
cache.set(cwd, { data: null, fetchedAt: dispatchedAt, expiresAt: now + this.failureWindow(`${cwd}:${budget}`, now) });
|
|
1098
|
+
return { data: null, rateLimitedUntil: suspended ? this.limiter.resetAt(budget, now) : null, backend };
|
|
1099
|
+
}
|
|
1100
|
+
this.limiter.noteSuccess(budget, dispatchedAt);
|
|
1101
|
+
this.failureWindows.delete(`${cwd}:${budget}`);
|
|
1102
|
+
cache.set(cwd, { data, fetchedAt: dispatchedAt, expiresAt: now + ttl });
|
|
1103
|
+
return { data, rateLimitedUntil: null, backend };
|
|
1104
|
+
}
|
|
1105
|
+
/** Current failure window for a key; escalates once per elapsed window, not per failure. */
|
|
1106
|
+
failureWindow(key, now) {
|
|
1107
|
+
const w = this.failureWindows.get(key);
|
|
1108
|
+
if (w && now < w.startedAt + w.ttl)
|
|
1109
|
+
return w.ttl;
|
|
1110
|
+
const level = w ? w.level + 1 : 0;
|
|
1111
|
+
const ttl = Math.min(FAILURE_WINDOW_MIN_MS * 2 ** level, FAILURE_WINDOW_MAX_MS);
|
|
1112
|
+
this.failureWindows.set(key, { level, startedAt: now, ttl });
|
|
1113
|
+
return ttl;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/** Degraded-mode text naming the real reason and the real backend (never "GitHub" on GitLab). */
|
|
1117
|
+
function describeMissing(outcome, what) {
|
|
1118
|
+
if (outcome.rateLimitedUntil !== null) {
|
|
1119
|
+
return `${outcome.backend} rate limited until ${new Date(outcome.rateLimitedUntil).toISOString()} — ${what} unavailable`;
|
|
1072
1120
|
}
|
|
1121
|
+
if (outcome.backend === null)
|
|
1122
|
+
return `forge concept disabled — ${what} unavailable`;
|
|
1123
|
+
return `${outcome.backend} CLI unavailable — could not fetch ${what}`;
|
|
1073
1124
|
}
|
|
1074
1125
|
//# sourceMappingURL=overview.js.map
|