@cluesmith/codev 3.3.1 → 3.3.3
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-CUIjS_Vb.js} +41 -41
- package/dashboard-dist/assets/index-CUIjS_Vb.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 +52 -1
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
- package/dist/agent-farm/commands/inbox.js +67 -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/constants.d.ts +118 -0
- package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/constants.js +118 -0
- package/dist/agent-farm/commands/reset/constants.js.map +1 -1
- package/dist/agent-farm/commands/reset/context.d.ts +18 -0
- package/dist/agent-farm/commands/reset/context.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/context.js +41 -0
- package/dist/agent-farm/commands/reset/context.js.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/receipt.js +16 -5
- package/dist/agent-farm/commands/reset/receipt.js.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts +1 -1
- package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -1
- package/dist/agent-farm/commands/reset/reorient.js +8 -1
- package/dist/agent-farm/commands/reset/reorient.js.map +1 -1
- package/dist/agent-farm/commands/reset/self.d.ts +335 -0
- package/dist/agent-farm/commands/reset/self.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/self.js +636 -0
- package/dist/agent-farm/commands/reset/self.js.map +1 -0
- 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 +29 -21
- package/dist/agent-farm/commands/reset.js.map +1 -1
- package/dist/agent-farm/commands/self-refresh.d.ts +60 -0
- package/dist/agent-farm/commands/self-refresh.d.ts.map +1 -0
- package/dist/agent-farm/commands/self-refresh.js +506 -0
- package/dist/agent-farm/commands/self-refresh.js.map +1 -0
- 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 +90 -7
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts +7 -0
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +13 -1
- package/dist/agent-farm/commands/spawn.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/types.d.ts +22 -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/tower-client.d.ts +12 -0
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +12 -0
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +43 -14
- 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 +21 -0
- package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
- package/dist/agent-farm/servers/gate-profiles.js +36 -4
- package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +218 -15
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +259 -27
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -11
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +202 -48
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/message-write.d.ts +58 -18
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +73 -28
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +37 -1
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +71 -12
- package/dist/agent-farm/servers/render-gate.js.map +1 -1
- package/dist/agent-farm/servers/session-submit.d.ts +220 -27
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
- package/dist/agent-farm/servers/session-submit.js +333 -42
- 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 +3 -2
- 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 +11 -3
- 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 +245 -36
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +15 -2
- 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 +53 -5
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +14 -0
- package/dist/agent-farm/servers/tower-tunnel.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 +8 -1
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/types.d.ts +36 -1
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.d.ts +136 -4
- package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
- package/dist/agent-farm/utils/message-format.js +212 -22
- package/dist/agent-farm/utils/message-format.js.map +1 -1
- package/dist/agent-farm/utils/server-utils.d.ts +8 -2
- package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
- package/dist/agent-farm/utils/server-utils.js +19 -2
- 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/doctor.js +2 -2
- package/dist/commands/generate-image.d.ts +20 -1
- package/dist/commands/generate-image.d.ts.map +1 -1
- package/dist/commands/generate-image.js +188 -1
- package/dist/commands/generate-image.js.map +1 -1
- package/dist/commands/porch/context-refresh.d.ts +181 -0
- package/dist/commands/porch/context-refresh.d.ts.map +1 -0
- package/dist/commands/porch/context-refresh.js +270 -0
- package/dist/commands/porch/context-refresh.js.map +1 -0
- package/dist/commands/porch/index.d.ts +6 -0
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +46 -0
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts +0 -9
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +133 -4
- package/dist/commands/porch/next.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/commands/porch/protocol.d.ts.map +1 -1
- package/dist/commands/porch/protocol.js +102 -0
- package/dist/commands/porch/protocol.js.map +1 -1
- package/dist/commands/porch/types.d.ts +61 -0
- package/dist/commands/porch/types.d.ts.map +1 -1
- package/dist/lib/forge-contracts.d.ts +66 -5
- package/dist/lib/forge-contracts.d.ts.map +1 -1
- package/dist/lib/forge-contracts.js.map +1 -1
- package/dist/lib/forge.d.ts +1 -1
- package/dist/lib/forge.d.ts.map +1 -1
- package/dist/lib/forge.js +33 -11
- package/dist/lib/forge.js.map +1 -1
- package/dist/lib/self-refresh-invocation.d.ts +60 -0
- package/dist/lib/self-refresh-invocation.d.ts.map +1 -0
- package/dist/lib/self-refresh-invocation.js +64 -0
- package/dist/lib/self-refresh-invocation.js.map +1 -0
- package/dist/lib/test-env.d.ts +50 -0
- package/dist/lib/test-env.d.ts.map +1 -1
- package/dist/lib/test-env.js +76 -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 +52 -4
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +157 -18
- package/dist/terminal/pty-session.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/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-search.sh +3 -1
- 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 +3 -1
- package/scripts/forge/linear/issue-view.sh +13 -2
- package/skeleton/.claude/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.claude/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/.claude/skills/generate-image/SKILL.md +22 -1
- package/skeleton/.codex/skills/arch-save/SKILL.md +8 -2
- package/skeleton/.codex/skills/builder-refresh/SKILL.md +138 -0
- package/skeleton/.codex/skills/generate-image/SKILL.md +22 -1
- package/skeleton/protocol-schema.json +17 -0
- package/skeleton/protocols/air/prompts/pr.md +6 -1
- package/skeleton/protocols/air/protocol.json +1 -1
- package/skeleton/protocols/aspir/prompts/review.md +6 -1
- package/skeleton/protocols/aspir/protocol.json +5 -1
- package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
- package/skeleton/protocols/bugfix/protocol.json +1 -1
- package/skeleton/protocols/bugfix/protocol.md +2 -1
- package/skeleton/protocols/experiment/protocol.json +1 -1
- package/skeleton/protocols/maintain/prompts/review.md +6 -1
- package/skeleton/protocols/maintain/protocol.json +1 -1
- package/skeleton/protocols/pir/prompts/review.md +10 -7
- package/skeleton/protocols/pir/protocol.json +1 -1
- package/skeleton/protocols/protocol-schema.json +17 -0
- package/skeleton/protocols/research/protocol.json +1 -1
- package/skeleton/protocols/spike/protocol.json +1 -1
- package/skeleton/protocols/spir/prompts/review.md +6 -1
- package/skeleton/protocols/spir/protocol.json +5 -1
- package/skeleton/resources/commands/agent-farm.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
|
@@ -26,7 +26,7 @@ import { getBuilders, setArchitectByName } from '../state.js';
|
|
|
26
26
|
import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
|
|
27
27
|
import { parseJsonBody, isRequestAllowed, isAllowedOrigin, isAllowedHost, getExpectedKey, escapeHtml } from '../utils/server-utils.js';
|
|
28
28
|
import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER, VSCODE_USER_SENDER } from '@cluesmith/codev-types';
|
|
29
|
-
import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
|
|
29
|
+
import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, persistableCommand, logSessionIdentity, } from './tower-utils.js';
|
|
30
30
|
import { handleTunnelEndpoint } from './tower-tunnel.js';
|
|
31
31
|
import { getWorktreeConfig, getActivityHooks } from '../utils/config.js';
|
|
32
32
|
import { ensureCodevConfigWatcher } from './codev-config-watcher.js';
|
|
@@ -35,16 +35,18 @@ import { fetchTeamGitHubData } from '../../lib/team-github.js';
|
|
|
35
35
|
import { resolveTarget, resolveAgentInRegistry, broadcastMessage, isResolveError } from './tower-messages.js';
|
|
36
36
|
import { handleCommandRoute, COMMAND_ROUTE } from './command-relay.js';
|
|
37
37
|
import { handleCanvasRoute, CANVAS_ROUTE_PREFIX } from './canvas-relay.js';
|
|
38
|
-
import { formatArchitectMessage, formatBuilderMessage, formatUserViaVsCodeMessage } from '../utils/message-format.js';
|
|
38
|
+
import { formatArchitectMessage, formatArchitectToBuilderMessage, formatBuilderMessage, formatUserViaVsCodeMessage, isSafeSenderId, messageTooLargeError, MAX_MESSAGE_BYTES, } from '../utils/message-format.js';
|
|
39
39
|
import { writeMessageToSession, writeEscapeToSession } from './message-write.js';
|
|
40
40
|
import { makeDeliveryPorts, getMailboxDrainer } from './mailbox-wiring.js';
|
|
41
41
|
import { deliverAgentMailSerialized } from './mailbox-delivery.js';
|
|
42
42
|
import { deliverCronMail, CRON_SENDER } from './cron-delivery.js';
|
|
43
43
|
import { enqueue as enqueueMailbox, getById as getMailboxById, markDelivered as markMailboxDelivered, listHeld as listHeldMailbox, dismiss as dismissMailbox, } from '../db/mailbox.js';
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
44
|
+
import { formatVerdict } from '@cluesmith/codev-sdk/hold-verdict';
|
|
45
|
+
// Spec 1273 per-terminal submission lock. Originally the anti-fusion lock for the two
|
|
46
|
+
// explicit human-bypass paths (escape + interrupt); since Issue #1365 the gated mailbox
|
|
47
|
+
// delivery path takes it too, so it now serializes the whole write edge of a terminal.
|
|
48
|
+
// The operator paths pass a wait ceiling — see OPERATOR_SUBMIT_WAIT_CEILING_MS.
|
|
49
|
+
import { submitToSession, OPERATOR_SUBMIT_WAIT_CEILING_MS } from './session-submit.js';
|
|
48
50
|
// Spec 1307 `--delay` — Tower-side deferred delivery, re-homed onto the Spec 1313
|
|
49
51
|
// mailbox (the merge that carried this feature was flattened by a later rebase, so it
|
|
50
52
|
// is grafted here explicitly): the due-time callback enqueues to the mailbox and
|
|
@@ -649,7 +651,12 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
649
651
|
else {
|
|
650
652
|
entry.shells.set(roleId, session.id);
|
|
651
653
|
}
|
|
652
|
-
|
|
654
|
+
// PIR #1475: prefer the identity the shellper reports. A fresh spawn is
|
|
655
|
+
// the no-op case (it reports back what we just asked for), but routing
|
|
656
|
+
// it through the same accessor keeps one rule everywhere: the row
|
|
657
|
+
// records what is RUNNING, not what was requested.
|
|
658
|
+
logSessionIdentity(ctx.log, 'terminal-create', session.id, ptySession, command ?? null);
|
|
659
|
+
saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, label ?? null, cwd ?? null, persistableCommand(ptySession) ?? command ?? null);
|
|
653
660
|
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
|
|
654
661
|
}
|
|
655
662
|
}
|
|
@@ -764,8 +771,20 @@ async function handleTerminalRoutes(req, res, url, match) {
|
|
|
764
771
|
}
|
|
765
772
|
const info = manager.resizeSession(terminalId, body.cols, body.rows);
|
|
766
773
|
if (!info) {
|
|
767
|
-
|
|
768
|
-
|
|
774
|
+
// Issue #1482: a resize that did not reach the process is NOT a missing session, and it
|
|
775
|
+
// is not a success either — echoing back the requested dimensions (what a 200 here used
|
|
776
|
+
// to do) is how Tower came to believe a geometry the TUI never adopted.
|
|
777
|
+
if (!manager.getSession(terminalId)) {
|
|
778
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
779
|
+
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
783
|
+
res.end(JSON.stringify({
|
|
784
|
+
error: 'RESIZE_DROPPED',
|
|
785
|
+
message: `Session ${terminalId} did not accept the resize (no live process or dropped shellper write); dimensions unchanged`,
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
769
788
|
return;
|
|
770
789
|
}
|
|
771
790
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1274,17 +1293,33 @@ function liveTargetIdentity(result) {
|
|
|
1274
1293
|
const archName = architectNameForTerminal(result.workspacePath, result.terminalId);
|
|
1275
1294
|
return { toAgent: archName ?? result.agent, isArchitectTarget: archName !== null };
|
|
1276
1295
|
}
|
|
1277
|
-
/**
|
|
1278
|
-
|
|
1296
|
+
/**
|
|
1297
|
+
* Format a message per sender/target — preserves the pre-1313 formatting rules.
|
|
1298
|
+
*
|
|
1299
|
+
* #1574: `toAgent` is the CANONICAL recipient the mailbox row is keyed on (the
|
|
1300
|
+
* architect reverse-map for architect targets, the resolved agent id otherwise),
|
|
1301
|
+
* so the header a recipient reads names the same identity the row was addressed
|
|
1302
|
+
* to. Passing anything else here would make the frame attest to a fiction.
|
|
1303
|
+
*
|
|
1304
|
+
* #1478: `from` is the SENDER, and it is threaded on the any → builder path too —
|
|
1305
|
+
* that branch used to discard it, so an `architect:<name>` origin reached the
|
|
1306
|
+
* builder's composer as a bare `ARCHITECT INSTRUCTION`. The two are complementary:
|
|
1307
|
+
* #1574 answers "was this addressed to me?", #1478 answers "who sent it?".
|
|
1308
|
+
*/
|
|
1309
|
+
function formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw) {
|
|
1279
1310
|
// #1494: a human's approval relayed by the VS Code extension gets its own
|
|
1280
1311
|
// header, so the architect can tell it from a peer-architect instruction.
|
|
1281
1312
|
if (isArchitectTarget && from === VSCODE_USER_SENDER)
|
|
1282
|
-
return formatUserViaVsCodeMessage(message, undefined, raw);
|
|
1313
|
+
return formatUserViaVsCodeMessage(toAgent, message, undefined, raw);
|
|
1283
1314
|
if (isArchitectTarget && from)
|
|
1284
|
-
return formatBuilderMessage(from, message, undefined, raw); // builder → architect
|
|
1315
|
+
return formatBuilderMessage(from, toAgent, message, undefined, raw); // builder → architect
|
|
1316
|
+
// any → builder: thread the sender so an `architect:<name>` origin is named in the
|
|
1317
|
+
// header (issue #1478) instead of collapsing to a bare `ARCHITECT INSTRUCTION`.
|
|
1285
1318
|
if (!isArchitectTarget)
|
|
1286
|
-
return
|
|
1287
|
-
|
|
1319
|
+
return formatArchitectToBuilderMessage(toAgent, message, undefined, raw, from); // any → builder
|
|
1320
|
+
// unknown → architect: `from` is absent by construction on this branch, so there is
|
|
1321
|
+
// no sender to name.
|
|
1322
|
+
return raw ? message : formatArchitectMessage(toAgent, message, undefined, false); // unknown → architect
|
|
1288
1323
|
}
|
|
1289
1324
|
/**
|
|
1290
1325
|
* Route a cron notification through the Spec 1313 mailbox + gate — the Tower-wired
|
|
@@ -1304,9 +1339,12 @@ export async function deliverCronMessage(task, message, log) {
|
|
|
1304
1339
|
const ports = makeDeliveryPorts(log);
|
|
1305
1340
|
// Preserve the pre-1313 cron framing: a message FROM the `af-cron` pseudo-builder,
|
|
1306
1341
|
// regardless of whether the target is an architect or a builder.
|
|
1342
|
+
//
|
|
1343
|
+
// #1574: the frame names its recipient, which is only known AFTER resolution —
|
|
1344
|
+
// so it is built per-branch rather than once up front. `base` carries everything
|
|
1345
|
+
// that does not depend on the target.
|
|
1307
1346
|
const base = {
|
|
1308
1347
|
body: message,
|
|
1309
|
-
formattedMessage: formatBuilderMessage(CRON_SENDER, message),
|
|
1310
1348
|
supersedeKey: task.name,
|
|
1311
1349
|
};
|
|
1312
1350
|
const live = resolveTarget(task.target, task.workspacePath);
|
|
@@ -1314,6 +1352,7 @@ export async function deliverCronMessage(task, message, log) {
|
|
|
1314
1352
|
const { toAgent } = liveTargetIdentity(live);
|
|
1315
1353
|
return deliverCronMail(ports, db, {
|
|
1316
1354
|
...base,
|
|
1355
|
+
formattedMessage: formatBuilderMessage(CRON_SENDER, toAgent, message),
|
|
1317
1356
|
workspacePath: live.workspacePath,
|
|
1318
1357
|
toAgent,
|
|
1319
1358
|
terminalId: live.terminalId,
|
|
@@ -1327,6 +1366,7 @@ export async function deliverCronMessage(task, message, log) {
|
|
|
1327
1366
|
if (!isResolveError(reg)) {
|
|
1328
1367
|
return deliverCronMail(ports, db, {
|
|
1329
1368
|
...base,
|
|
1369
|
+
formattedMessage: formatBuilderMessage(CRON_SENDER, reg.agent, message),
|
|
1330
1370
|
workspacePath: reg.workspacePath,
|
|
1331
1371
|
toAgent: reg.agent,
|
|
1332
1372
|
terminalId: null,
|
|
@@ -1334,7 +1374,7 @@ export async function deliverCronMessage(task, message, log) {
|
|
|
1334
1374
|
}
|
|
1335
1375
|
}
|
|
1336
1376
|
log('WARN', `Cron '${task.name}': target '${task.target}' not found — message not delivered`);
|
|
1337
|
-
return { outcome: 'unresolved', reason: null, mailboxId: null };
|
|
1377
|
+
return { outcome: 'unresolved', reason: null, detail: null, mailboxId: null };
|
|
1338
1378
|
}
|
|
1339
1379
|
/**
|
|
1340
1380
|
* Persist a `held` mailbox row and write the Spec 1313 `held` send response. Used
|
|
@@ -1355,9 +1395,31 @@ function holdAndRespond(res, ctx, input, reason) {
|
|
|
1355
1395
|
delivered: false,
|
|
1356
1396
|
held: true,
|
|
1357
1397
|
reason,
|
|
1398
|
+
// A hold from THIS path is always a non-gate one (no live PTY), so it carries no gate
|
|
1399
|
+
// detail (Issue #1482). Reported explicitly rather than omitted so the held response has
|
|
1400
|
+
// one shape whichever path produced it.
|
|
1401
|
+
detail: null,
|
|
1358
1402
|
mailboxId: row.id,
|
|
1403
|
+
bodyLength: Buffer.byteLength(input.body, 'utf8'),
|
|
1359
1404
|
});
|
|
1360
1405
|
}
|
|
1406
|
+
/** Machine-readable reason paired with `degraded: true` on an operator send response. */
|
|
1407
|
+
const DEGRADED_SUBMIT_REASON = 'submit-wait-ceiling-expired';
|
|
1408
|
+
/**
|
|
1409
|
+
* Announce that an operator submission gave up waiting for the per-terminal lock and wrote
|
|
1410
|
+
* unserialized (Issue #1365).
|
|
1411
|
+
*
|
|
1412
|
+
* This is the degraded path, and it is deliberately loud. The ceiling arms only against a
|
|
1413
|
+
* DELIVERY write (behind another operator the wait stays unbounded, as before #1365), so what
|
|
1414
|
+
* expiry falls back to is exactly the pre-#1365 operator-vs-delivery behaviour: two disjoint
|
|
1415
|
+
* locks, no serialization. That one pair is therefore no worse than the old status quo — but
|
|
1416
|
+
* it used to be invisible, and now it is not, here and in the `degraded` flag on the response.
|
|
1417
|
+
*/
|
|
1418
|
+
function logCeilingExpired(ctx, action, toAgent, terminalId, waitedMs) {
|
|
1419
|
+
ctx.log('WARN', `${action} → ${toAgent} (terminal ${terminalId.slice(0, 8)}...) waited ${waitedMs}ms for an in-flight ` +
|
|
1420
|
+
`write and proceeded UNSERIALIZED — it may interleave with that write. A message long enough to ` +
|
|
1421
|
+
`hold the line this long is the usual cause.`);
|
|
1422
|
+
}
|
|
1361
1423
|
/**
|
|
1362
1424
|
* Handle a delayed (`--delay`) send (Spec 1307, re-homed onto the Spec 1313 mailbox; round 3
|
|
1363
1425
|
* durable rework). The row is RESOLVED, authorized (the builder-spoofing check inside
|
|
@@ -1420,7 +1482,7 @@ function handleDelayedSend(res, ctx, db, params) {
|
|
|
1420
1482
|
sendJson(res, statusCode, { error: errorCode, message: live.message });
|
|
1421
1483
|
return;
|
|
1422
1484
|
}
|
|
1423
|
-
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, message, raw);
|
|
1485
|
+
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw);
|
|
1424
1486
|
// Persist NOW, at request time, with the due time. reason=null → SCHEDULED, not stuck.
|
|
1425
1487
|
const row = enqueueMailbox(db, {
|
|
1426
1488
|
workspacePath,
|
|
@@ -1462,6 +1524,17 @@ function handleDelayedSend(res, ctx, db, params) {
|
|
|
1462
1524
|
live.write('\x03'); // Ctrl+C only — end the turn; body follows via the gate
|
|
1463
1525
|
fired = true;
|
|
1464
1526
|
return 0; // no body, no Enter on this path
|
|
1527
|
+
}, undefined,
|
|
1528
|
+
// The same operator ceiling as the immediate paths (Issue #1365). This one fires
|
|
1529
|
+
// UNATTENDED, so it is the writer most likely to meet a gated delivery mid-pace —
|
|
1530
|
+
// which is precisely why the delayed `^C` must take this lock at all.
|
|
1531
|
+
{
|
|
1532
|
+
waitCeilingMs: OPERATOR_SUBMIT_WAIT_CEILING_MS,
|
|
1533
|
+
onCeilingExpired: (waitedMs) => logCeilingExpired(ctx, 'delayed interrupt ^C', toAgent, terminalId, waitedMs),
|
|
1534
|
+
// This is the one operator path whose write can be a NO-OP (the liveness re-check
|
|
1535
|
+
// above). A degraded no-op raced nobody, so it must not bump the bypass counter —
|
|
1536
|
+
// that would make a concurrent delivery hold and re-deliver for nothing.
|
|
1537
|
+
wroteBytes: () => fired,
|
|
1465
1538
|
})
|
|
1466
1539
|
.then(() => ctx.log('INFO', fired
|
|
1467
1540
|
? `Delayed interrupt ^C fired → ${toAgent} (terminal ${terminalId.slice(0, 8)}...); body delivers via the gate`
|
|
@@ -1488,6 +1561,7 @@ function handleDelayedSend(res, ctx, db, params) {
|
|
|
1488
1561
|
deliverAfter,
|
|
1489
1562
|
mailboxId: row.id,
|
|
1490
1563
|
notBefore,
|
|
1564
|
+
bodyLength: Buffer.byteLength(params.message, 'utf8'),
|
|
1491
1565
|
});
|
|
1492
1566
|
}
|
|
1493
1567
|
async function handleSend(req, res, ctx) {
|
|
@@ -1505,8 +1579,38 @@ async function handleSend(req, res, ctx) {
|
|
|
1505
1579
|
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: 'Missing or empty "message" field' }));
|
|
1506
1580
|
return;
|
|
1507
1581
|
}
|
|
1582
|
+
// Issue #1573: refuse an over-large body LOUDLY, here at the route rather than only at the
|
|
1583
|
+
// CLI — `/api/send` is a public local route, and the CLI is not its only caller. Nothing
|
|
1584
|
+
// downstream bounded message size, so an oversized body used to be paced onto the line one
|
|
1585
|
+
// line at a time and reported delivered whatever the composer made of it. Rejecting is the
|
|
1586
|
+
// only honest option: truncating would reproduce #1564 on purpose.
|
|
1587
|
+
const bodyLength = Buffer.byteLength(message, 'utf8');
|
|
1588
|
+
if (bodyLength > MAX_MESSAGE_BYTES) {
|
|
1589
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1590
|
+
res.end(JSON.stringify({ error: 'MESSAGE_TOO_LARGE', message: messageTooLargeError(bodyLength) }));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1508
1593
|
// Optional fields
|
|
1509
1594
|
const from = typeof body.from === 'string' ? body.from : undefined;
|
|
1595
|
+
// Maintainer review (PR #1486): `from` was the one unguarded field on this route — the
|
|
1596
|
+
// body above is bounded by MAX_MESSAGE_BYTES, but the sender was taken verbatim, and
|
|
1597
|
+
// `/api/send` is a public local route whose only well-behaved caller is the CLI. It is
|
|
1598
|
+
// stored on the mailbox row and rendered by every attribution surface, and `afx inbox`
|
|
1599
|
+
// now sizes its `FROM → TO` column to the widest value it holds — so an unbounded
|
|
1600
|
+
// identity is a rendering cost paid by every row, not just its own. Enforced with the
|
|
1601
|
+
// SAME predicate the header formatter uses, so the boundary and the renderer cannot
|
|
1602
|
+
// disagree about what an identity is. Refused rather than silently dropped or
|
|
1603
|
+
// truncated: a message delivered under a quietly-discarded sender is exactly the
|
|
1604
|
+
// unattributed frame this PR exists to fix.
|
|
1605
|
+
if (from !== undefined && !isSafeSenderId(from)) {
|
|
1606
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1607
|
+
res.end(JSON.stringify({
|
|
1608
|
+
error: 'INVALID_PARAMS',
|
|
1609
|
+
message: 'Invalid "from" field: a sender identity must be 1-128 characters of ' +
|
|
1610
|
+
'[A-Za-z0-9._:-] (no whitespace, brackets or newlines).',
|
|
1611
|
+
}));
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1510
1614
|
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
|
|
1511
1615
|
const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
|
|
1512
1616
|
const options = typeof body.options === 'object' && body.options !== null
|
|
@@ -1571,7 +1675,7 @@ async function handleSend(req, res, ctx) {
|
|
|
1571
1675
|
workspacePath: reg.workspacePath,
|
|
1572
1676
|
toAgent: reg.agent,
|
|
1573
1677
|
body: message,
|
|
1574
|
-
formattedMessage: formatMessageForTarget(reg.kind === 'architect', from, message, raw),
|
|
1678
|
+
formattedMessage: formatMessageForTarget(reg.kind === 'architect', from, reg.agent, message, raw),
|
|
1575
1679
|
fromAgent: from ?? null,
|
|
1576
1680
|
fromWorkspace: senderWorkspace,
|
|
1577
1681
|
noEnter,
|
|
@@ -1609,7 +1713,7 @@ async function handleSend(req, res, ctx) {
|
|
|
1609
1713
|
workspacePath: result.workspacePath,
|
|
1610
1714
|
toAgent,
|
|
1611
1715
|
body: message,
|
|
1612
|
-
formattedMessage: formatMessageForTarget(isArchitectTarget, from, message, raw),
|
|
1716
|
+
formattedMessage: formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw),
|
|
1613
1717
|
fromAgent: from ?? null,
|
|
1614
1718
|
fromWorkspace: senderWorkspace,
|
|
1615
1719
|
noEnter,
|
|
@@ -1634,7 +1738,7 @@ async function handleSend(req, res, ctx) {
|
|
|
1634
1738
|
workspacePath: result.workspacePath,
|
|
1635
1739
|
toAgent,
|
|
1636
1740
|
body: message,
|
|
1637
|
-
formattedMessage: formatMessageForTarget(isArchitectTarget, from, message, raw),
|
|
1741
|
+
formattedMessage: formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw),
|
|
1638
1742
|
fromAgent: from ?? null,
|
|
1639
1743
|
fromWorkspace: senderWorkspace,
|
|
1640
1744
|
noEnter,
|
|
@@ -1650,7 +1754,17 @@ async function handleSend(req, res, ctx) {
|
|
|
1650
1754
|
if (escape) {
|
|
1651
1755
|
// Awaited: the response must not claim delivery before the ESC and its
|
|
1652
1756
|
// Enter have actually been written (Spec 1273 verify).
|
|
1653
|
-
|
|
1757
|
+
// Issue #1365: escape is an operator submission — it blocks behind an in-flight write
|
|
1758
|
+
// (including a gated mailbox delivery, which now takes this same lock) so its ESC and
|
|
1759
|
+
// Enter can no longer truncate a delivery mid-pace, but only up to the wait ceiling.
|
|
1760
|
+
let escapeDegraded = false;
|
|
1761
|
+
await submitToSession(result.terminalId, () => writeEscapeToSession(session, noEnter), undefined, {
|
|
1762
|
+
waitCeilingMs: OPERATOR_SUBMIT_WAIT_CEILING_MS,
|
|
1763
|
+
onCeilingExpired: (waitedMs) => {
|
|
1764
|
+
escapeDegraded = true;
|
|
1765
|
+
logCeilingExpired(ctx, 'ESC', toAgent, result.terminalId, waitedMs);
|
|
1766
|
+
},
|
|
1767
|
+
});
|
|
1654
1768
|
broadcastMessage({
|
|
1655
1769
|
type: 'message',
|
|
1656
1770
|
from: { project: path.basename(senderWorkspace), agent: from ?? 'unknown' },
|
|
@@ -1660,10 +1774,16 @@ async function handleSend(req, res, ctx) {
|
|
|
1660
1774
|
timestamp: new Date().toISOString(),
|
|
1661
1775
|
});
|
|
1662
1776
|
ctx.log('INFO', `Interrupt (ESC) sent: ${from ?? 'unknown'} → ${toAgent} (terminal ${result.terminalId.slice(0, 8)}...)`);
|
|
1663
|
-
sendJson(res, 200, {
|
|
1777
|
+
sendJson(res, 200, {
|
|
1778
|
+
ok: true,
|
|
1779
|
+
terminalId: result.terminalId,
|
|
1780
|
+
resolvedTo: toAgent,
|
|
1781
|
+
deferred: false,
|
|
1782
|
+
...(escapeDegraded ? { degraded: true, degradedReason: DEGRADED_SUBMIT_REASON } : {}),
|
|
1783
|
+
});
|
|
1664
1784
|
return;
|
|
1665
1785
|
}
|
|
1666
|
-
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, message, raw);
|
|
1786
|
+
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw);
|
|
1667
1787
|
// NB: `--delay` (deliverAfter) is handled earlier by handleDelayedSend, before this
|
|
1668
1788
|
// immediate-path block — a delayed send is resolved + persisted at request time and does
|
|
1669
1789
|
// not fall through here.
|
|
@@ -1701,15 +1821,29 @@ async function handleSend(req, res, ctx) {
|
|
|
1701
1821
|
// during the 100 ms gap. `writeMessageToSession(..., 100)` schedules the text 100 ms after
|
|
1702
1822
|
// the ^C (the settle) and returns the completion offset, so the lock is held until the
|
|
1703
1823
|
// whole interrupt is on the wire; uncontended, it runs at once.
|
|
1704
|
-
// Scope of the guarantee
|
|
1705
|
-
//
|
|
1706
|
-
// concurrent
|
|
1707
|
-
//
|
|
1708
|
-
//
|
|
1709
|
-
//
|
|
1824
|
+
// Scope of the guarantee (Issue #1365 — this used to end at "interrupt vs interrupt/escape"):
|
|
1825
|
+
// the gated mailbox delivery path now takes this SAME per-terminal lock, so an interrupt is
|
|
1826
|
+
// serialized against a concurrent delivery too. That closes the case where a `^C` landed
|
|
1827
|
+
// inside a delivery's own text→Enter window, clearing the composer while every byte still
|
|
1828
|
+
// reported success — the row read `delivered` for a message the agent never saw. The lock
|
|
1829
|
+
// order is per-agent → per-terminal (the delivery takes this one as a leaf), never the
|
|
1830
|
+
// reverse, so there is no cycle. See session-submit.ts for the full boundary.
|
|
1831
|
+
// Whether this interrupt gave up waiting and wrote unserialized. It must reach the SENDER,
|
|
1832
|
+
// not just the Tower log (codex review of PR #1492): the row above was claimed `delivered`
|
|
1833
|
+
// before the write, so a degraded interrupt would otherwise report an unqualified success
|
|
1834
|
+
// for a body that may have interleaved with the delivery it skipped. We keep the
|
|
1835
|
+
// claim-first tradeoff (un-claiming risks a double delivery — see above) and tell the
|
|
1836
|
+
// truth about it instead.
|
|
1837
|
+
let degraded = false;
|
|
1710
1838
|
await submitToSession(result.terminalId, () => {
|
|
1711
1839
|
session.write('\x03'); // Ctrl+C
|
|
1712
1840
|
return writeMessageToSession(session, formattedMessage, noEnter, 100);
|
|
1841
|
+
}, undefined, {
|
|
1842
|
+
waitCeilingMs: OPERATOR_SUBMIT_WAIT_CEILING_MS,
|
|
1843
|
+
onCeilingExpired: (waitedMs) => {
|
|
1844
|
+
degraded = true;
|
|
1845
|
+
logCeilingExpired(ctx, 'interrupt', toAgent, result.terminalId, waitedMs);
|
|
1846
|
+
},
|
|
1713
1847
|
});
|
|
1714
1848
|
broadcastMessage({
|
|
1715
1849
|
type: 'message',
|
|
@@ -1729,6 +1863,10 @@ async function handleSend(req, res, ctx) {
|
|
|
1729
1863
|
held: false,
|
|
1730
1864
|
mailboxId: row.id,
|
|
1731
1865
|
reason: null,
|
|
1866
|
+
detail: null,
|
|
1867
|
+
bodyLength,
|
|
1868
|
+
// Additive and present only when it happened, so older clients are unaffected.
|
|
1869
|
+
...(degraded ? { degraded: true, degradedReason: DEGRADED_SUBMIT_REASON } : {}),
|
|
1732
1870
|
});
|
|
1733
1871
|
return;
|
|
1734
1872
|
}
|
|
@@ -1755,8 +1893,12 @@ async function handleSend(req, res, ctx) {
|
|
|
1755
1893
|
...basePorts,
|
|
1756
1894
|
getSessionForAgent: (ws, agent) => ws === result.workspacePath && agent === toAgent ? session : basePorts.getSessionForAgent(ws, agent),
|
|
1757
1895
|
};
|
|
1896
|
+
// Issue #1584: keep the outcome — it carries whether the delivered message's header was
|
|
1897
|
+
// actually seen on the receiving terminal, which is the only thing the sender can act on now
|
|
1898
|
+
// that an unconfirmed delivery is recorded rather than re-written.
|
|
1899
|
+
let outcome = null;
|
|
1758
1900
|
try {
|
|
1759
|
-
await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
|
|
1901
|
+
outcome = await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
|
|
1760
1902
|
}
|
|
1761
1903
|
catch (err) {
|
|
1762
1904
|
// A gate/write error leaves the row HELD (markDelivered only runs on a
|
|
@@ -1775,11 +1917,25 @@ async function handleSend(req, res, ctx) {
|
|
|
1775
1917
|
held: false,
|
|
1776
1918
|
mailboxId: row.id,
|
|
1777
1919
|
reason: null,
|
|
1920
|
+
bodyLength,
|
|
1921
|
+
// Additive (Issue #1584). `false` means the bytes were written and accepted but the header
|
|
1922
|
+
// never appeared on the terminal — delivered, flagged, and NOT re-written. Reported only
|
|
1923
|
+
// when THIS row is the one our own pass delivered: a pass picks the agent's OLDEST held
|
|
1924
|
+
// row, so without the id check a concurrent drainer delivery of this row could be
|
|
1925
|
+
// reported with another message's verification. Absent (verification skipped, or someone
|
|
1926
|
+
// else's pass delivered it) reads exactly as it did before this field existed.
|
|
1927
|
+
...(outcome?.delivered.includes(row.id) && outcome.verified !== undefined
|
|
1928
|
+
? { verified: outcome.verified }
|
|
1929
|
+
: {}),
|
|
1778
1930
|
});
|
|
1779
1931
|
return;
|
|
1780
1932
|
}
|
|
1781
1933
|
const reason = stored?.reason ?? 'busy';
|
|
1782
|
-
|
|
1934
|
+
// Issue #1482: read the detail back off the row the delivery pass just refreshed, rather
|
|
1935
|
+
// than re-deriving it — the row is the record every other surface reads, so the sender's
|
|
1936
|
+
// response and `afx inbox` can never disagree about the same hold.
|
|
1937
|
+
const detail = stored?.detail ?? null;
|
|
1938
|
+
ctx.log('INFO', `Message held (${formatVerdict(reason, detail)}): ${from ?? 'unknown'} → ${toAgent} (mailbox ${row.id.slice(0, 8)}...)`);
|
|
1783
1939
|
// The message stayed held → a new held row is in the set; refresh the indicator
|
|
1784
1940
|
// count (Spec 1313, Phase 7). The delivered branch above needs no fire — the
|
|
1785
1941
|
// delivery path's onHeldStateChange already broadcast when the row left the set.
|
|
@@ -1792,7 +1948,9 @@ async function handleSend(req, res, ctx) {
|
|
|
1792
1948
|
delivered: false,
|
|
1793
1949
|
held: true,
|
|
1794
1950
|
reason,
|
|
1951
|
+
detail,
|
|
1795
1952
|
mailboxId: row.id,
|
|
1953
|
+
bodyLength,
|
|
1796
1954
|
});
|
|
1797
1955
|
}
|
|
1798
1956
|
/**
|
|
@@ -1805,20 +1963,55 @@ async function handleSend(req, res, ctx) {
|
|
|
1805
1963
|
* redaction rule): id, addresses, why-held reason, escalation flag, and enqueue time —
|
|
1806
1964
|
* the message BODY is deliberately never surfaced here (it travels only over the live
|
|
1807
1965
|
* terminal stream on delivery). `escalated` is normalized from SQLite's 0/1 to a bool.
|
|
1966
|
+
*
|
|
1967
|
+
* Issue 1450: `workspaceOverride` lets the workspace-scoped route
|
|
1968
|
+
* (`/workspace/<base64>/api/inbox`, which backs the dashboard's held-mail popover) pass the
|
|
1969
|
+
* workspace resolved from the URL prefix, exactly as `handleOverview` / `handleAnalytics` do.
|
|
1970
|
+
* The override arrives already normalized by the prefix decoder, so `normalizeWorkspacePath`
|
|
1971
|
+
* re-running on it is a safe no-op.
|
|
1972
|
+
*
|
|
1973
|
+
* Two separate widening hazards are closed here, because a scoped caller must never receive
|
|
1974
|
+
* another workspace's held mail:
|
|
1975
|
+
* - `??` (not `||`) means `?workspace=` can never take effect once an override was passed.
|
|
1976
|
+
* - An EMPTY override does not fall through to the unscoped all-workspaces listing; it
|
|
1977
|
+
* scopes to a path that matches nothing. Today the dispatcher 400s a missing prefix so
|
|
1978
|
+
* this is unreachable, but "unreachable" is a property of the caller, not of this
|
|
1979
|
+
* function, and the safe failure for a scoped call is zero rows, never every row.
|
|
1980
|
+
* The all-workspaces listing therefore remains reachable only when NO override was passed —
|
|
1981
|
+
* the Tower-level route without `?workspace=`, the direct-caller convenience the CLI never uses.
|
|
1982
|
+
*
|
|
1983
|
+
* NOTE: this lists ALL held rows, including pre-due `--delay` rows, while the badge count
|
|
1984
|
+
* (`heldSummaryForWorkspace`) excludes them — see the `HeldMessage` type for why the two
|
|
1985
|
+
* legitimately disagree.
|
|
1808
1986
|
*/
|
|
1809
|
-
|
|
1810
|
-
|
|
1987
|
+
// Exported as a narrow test seam (Issue 1450): the empty-override branch below cannot be
|
|
1988
|
+
// reached through `handleRequest` — the workspace dispatcher rejects a missing or non-absolute
|
|
1989
|
+
// prefix with a 400 before this runs — so the only way to pin that guard's behaviour is to call
|
|
1990
|
+
// the handler directly. Production callers go through the two route tables, not this export.
|
|
1991
|
+
export function handleInboxList(res, url, workspaceOverride) {
|
|
1992
|
+
const scoped = workspaceOverride !== undefined;
|
|
1993
|
+
const rawWorkspace = workspaceOverride ?? url.searchParams.get('workspace');
|
|
1811
1994
|
// Normalize to the stored realpath key (mailbox workspace_path is normalized at
|
|
1812
1995
|
// enqueue — tower-routes handleSend / holdAndRespond — matching overview.ts). Without
|
|
1813
1996
|
// this a symlinked workspace root would miss its own held rows.
|
|
1814
|
-
|
|
1997
|
+
// `scoped ? '' : undefined` on the empty branch: '' matches no workspace_path, so a scoped
|
|
1998
|
+
// call with a blank override returns nothing rather than widening to every workspace.
|
|
1999
|
+
const workspace = rawWorkspace
|
|
2000
|
+
? normalizeWorkspacePath(rawWorkspace)
|
|
2001
|
+
: (scoped ? '' : undefined);
|
|
1815
2002
|
const rows = listHeldMailbox(getGlobalDb(), workspace);
|
|
2003
|
+
// Annotated, not inferred: `HeldMessage` is the shared contract this route owes its
|
|
2004
|
+
// clients, so a projection that drifts from it (a dropped field, or a `body` slipping in)
|
|
2005
|
+
// fails the server build rather than only surprising the dashboard at runtime.
|
|
1816
2006
|
const projected = rows.map((r) => ({
|
|
1817
2007
|
id: r.id,
|
|
1818
2008
|
workspacePath: r.workspace_path,
|
|
1819
2009
|
toAgent: r.to_agent,
|
|
1820
2010
|
fromAgent: r.from_agent,
|
|
1821
2011
|
reason: r.reason,
|
|
2012
|
+
// Issue #1482: the gate verdict behind a `busy` hold, so `afx inbox` and the dashboard
|
|
2013
|
+
// popover can distinguish an occupied composer from one the classifier could not read.
|
|
2014
|
+
detail: r.detail,
|
|
1822
2015
|
escalated: r.escalated === 1,
|
|
1823
2016
|
createdAt: r.created_at,
|
|
1824
2017
|
// Spec 1313 round 3: due time of a pre-due delayed (`--delay`) row; null = deliver-ASAP.
|
|
@@ -1884,6 +2077,7 @@ function handleInboxShow(req, res, match) {
|
|
|
1884
2077
|
fromWorkspace: row.from_workspace,
|
|
1885
2078
|
status: row.status,
|
|
1886
2079
|
reason: row.reason,
|
|
2080
|
+
detail: row.detail, // Issue #1482: the gate verdict behind a `busy` hold
|
|
1887
2081
|
escalated: row.escalated === 1,
|
|
1888
2082
|
body: row.body,
|
|
1889
2083
|
createdAt: row.created_at,
|
|
@@ -2303,6 +2497,19 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
|
|
|
2303
2497
|
if (req.method === 'GET' && apiPath === 'overview') {
|
|
2304
2498
|
return handleOverview(res, url, workspacePath, ctx);
|
|
2305
2499
|
}
|
|
2500
|
+
// GET /api/inbox - held mailbox rows for THIS workspace (Issue 1450). Backs the
|
|
2501
|
+
// dashboard's clickable held-mail counter. Reuses the Tower-level handler with the
|
|
2502
|
+
// workspace resolved from the /workspace/<base64>/ prefix, the same way `overview`
|
|
2503
|
+
// and `analytics` above do — the dashboard calls relative `./api/...` and has no
|
|
2504
|
+
// absolute workspace path of its own to pass as `?workspace=`.
|
|
2505
|
+
//
|
|
2506
|
+
// Deliberately an EXACT match on 'inbox', not a prefix: `inbox/:id` (which returns the
|
|
2507
|
+
// message BODY) and `inbox/:id/dismiss` (which mutates) do not match, fall through to
|
|
2508
|
+
// the 404 below, and so stay off the dashboard surface. That is what keeps this read
|
|
2509
|
+
// path inside Spec 1313's redaction rule and decision 8 (dismissal is CLI-only).
|
|
2510
|
+
if (req.method === 'GET' && apiPath === 'inbox') {
|
|
2511
|
+
return handleInboxList(res, url, workspacePath);
|
|
2512
|
+
}
|
|
2306
2513
|
// POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
|
|
2307
2514
|
if (req.method === 'POST' && apiPath === 'overview/refresh') {
|
|
2308
2515
|
return handleOverviewRefresh(res, ctx);
|
|
@@ -2523,7 +2730,9 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
2523
2730
|
}
|
|
2524
2731
|
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
2525
2732
|
entry.shells.set(shellId, session.id);
|
|
2526
|
-
|
|
2733
|
+
// PIR #1475: hydrated identity wins; a fresh spawn reports back `shellCmd`.
|
|
2734
|
+
logSessionIdentity(ctx.log, 'shell-create', session.id, ptySession, shellCmd);
|
|
2735
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, session.label, workspacePath, persistableCommand(ptySession) ?? shellCmd);
|
|
2527
2736
|
shellCreated = true;
|
|
2528
2737
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2529
2738
|
res.end(JSON.stringify({
|