@cluesmith/codev 3.3.2 → 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/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.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 +64 -6
- package/dist/agent-farm/commands/send.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/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 +83 -8
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-delivery.js +102 -26
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +48 -15
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
- package/dist/agent-farm/servers/mailbox-wiring.js +116 -22
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
- package/dist/agent-farm/servers/render-gate.d.ts +24 -1
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
- package/dist/agent-farm/servers/render-gate.js +50 -12
- package/dist/agent-farm/servers/render-gate.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 +133 -13
- 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-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/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 +102 -5
- package/dist/agent-farm/utils/message-format.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/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/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/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/generate-image/SKILL.md +22 -1
- package/skeleton/.codex/skills/generate-image/SKILL.md +22 -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 +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,12 +35,13 @@ 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, formatArchitectToBuilderMessage, formatBuilderMessage, formatUserViaVsCodeMessage, messageTooLargeError, MAX_MESSAGE_BYTES, } 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
|
+
import { formatVerdict } from '@cluesmith/codev-sdk/hold-verdict';
|
|
44
45
|
// Spec 1273 per-terminal submission lock. Originally the anti-fusion lock for the two
|
|
45
46
|
// explicit human-bypass paths (escape + interrupt); since Issue #1365 the gated mailbox
|
|
46
47
|
// delivery path takes it too, so it now serializes the whole write edge of a terminal.
|
|
@@ -650,7 +651,12 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
650
651
|
else {
|
|
651
652
|
entry.shells.set(roleId, session.id);
|
|
652
653
|
}
|
|
653
|
-
|
|
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);
|
|
654
660
|
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
|
|
655
661
|
}
|
|
656
662
|
}
|
|
@@ -765,8 +771,20 @@ async function handleTerminalRoutes(req, res, url, match) {
|
|
|
765
771
|
}
|
|
766
772
|
const info = manager.resizeSession(terminalId, body.cols, body.rows);
|
|
767
773
|
if (!info) {
|
|
768
|
-
|
|
769
|
-
|
|
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
|
+
}
|
|
770
788
|
return;
|
|
771
789
|
}
|
|
772
790
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -1282,6 +1300,11 @@ function liveTargetIdentity(result) {
|
|
|
1282
1300
|
* architect reverse-map for architect targets, the resolved agent id otherwise),
|
|
1283
1301
|
* so the header a recipient reads names the same identity the row was addressed
|
|
1284
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?".
|
|
1285
1308
|
*/
|
|
1286
1309
|
function formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw) {
|
|
1287
1310
|
// #1494: a human's approval relayed by the VS Code extension gets its own
|
|
@@ -1290,8 +1313,12 @@ function formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw)
|
|
|
1290
1313
|
return formatUserViaVsCodeMessage(toAgent, message, undefined, raw);
|
|
1291
1314
|
if (isArchitectTarget && from)
|
|
1292
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`.
|
|
1293
1318
|
if (!isArchitectTarget)
|
|
1294
|
-
return formatArchitectToBuilderMessage(toAgent, message, undefined, raw); // any → builder
|
|
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.
|
|
1295
1322
|
return raw ? message : formatArchitectMessage(toAgent, message, undefined, false); // unknown → architect
|
|
1296
1323
|
}
|
|
1297
1324
|
/**
|
|
@@ -1347,7 +1374,7 @@ export async function deliverCronMessage(task, message, log) {
|
|
|
1347
1374
|
}
|
|
1348
1375
|
}
|
|
1349
1376
|
log('WARN', `Cron '${task.name}': target '${task.target}' not found — message not delivered`);
|
|
1350
|
-
return { outcome: 'unresolved', reason: null, mailboxId: null };
|
|
1377
|
+
return { outcome: 'unresolved', reason: null, detail: null, mailboxId: null };
|
|
1351
1378
|
}
|
|
1352
1379
|
/**
|
|
1353
1380
|
* Persist a `held` mailbox row and write the Spec 1313 `held` send response. Used
|
|
@@ -1368,6 +1395,10 @@ function holdAndRespond(res, ctx, input, reason) {
|
|
|
1368
1395
|
delivered: false,
|
|
1369
1396
|
held: true,
|
|
1370
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,
|
|
1371
1402
|
mailboxId: row.id,
|
|
1372
1403
|
bodyLength: Buffer.byteLength(input.body, 'utf8'),
|
|
1373
1404
|
});
|
|
@@ -1561,6 +1592,25 @@ async function handleSend(req, res, ctx) {
|
|
|
1561
1592
|
}
|
|
1562
1593
|
// Optional fields
|
|
1563
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
|
+
}
|
|
1564
1614
|
const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
|
|
1565
1615
|
const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
|
|
1566
1616
|
const options = typeof body.options === 'object' && body.options !== null
|
|
@@ -1813,6 +1863,7 @@ async function handleSend(req, res, ctx) {
|
|
|
1813
1863
|
held: false,
|
|
1814
1864
|
mailboxId: row.id,
|
|
1815
1865
|
reason: null,
|
|
1866
|
+
detail: null,
|
|
1816
1867
|
bodyLength,
|
|
1817
1868
|
// Additive and present only when it happened, so older clients are unaffected.
|
|
1818
1869
|
...(degraded ? { degraded: true, degradedReason: DEGRADED_SUBMIT_REASON } : {}),
|
|
@@ -1842,8 +1893,12 @@ async function handleSend(req, res, ctx) {
|
|
|
1842
1893
|
...basePorts,
|
|
1843
1894
|
getSessionForAgent: (ws, agent) => ws === result.workspacePath && agent === toAgent ? session : basePorts.getSessionForAgent(ws, agent),
|
|
1844
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;
|
|
1845
1900
|
try {
|
|
1846
|
-
await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
|
|
1901
|
+
outcome = await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
|
|
1847
1902
|
}
|
|
1848
1903
|
catch (err) {
|
|
1849
1904
|
// A gate/write error leaves the row HELD (markDelivered only runs on a
|
|
@@ -1863,11 +1918,24 @@ async function handleSend(req, res, ctx) {
|
|
|
1863
1918
|
mailboxId: row.id,
|
|
1864
1919
|
reason: null,
|
|
1865
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
|
+
: {}),
|
|
1866
1930
|
});
|
|
1867
1931
|
return;
|
|
1868
1932
|
}
|
|
1869
1933
|
const reason = stored?.reason ?? 'busy';
|
|
1870
|
-
|
|
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)}...)`);
|
|
1871
1939
|
// The message stayed held → a new held row is in the set; refresh the indicator
|
|
1872
1940
|
// count (Spec 1313, Phase 7). The delivered branch above needs no fire — the
|
|
1873
1941
|
// delivery path's onHeldStateChange already broadcast when the row left the set.
|
|
@@ -1880,6 +1948,7 @@ async function handleSend(req, res, ctx) {
|
|
|
1880
1948
|
delivered: false,
|
|
1881
1949
|
held: true,
|
|
1882
1950
|
reason,
|
|
1951
|
+
detail,
|
|
1883
1952
|
mailboxId: row.id,
|
|
1884
1953
|
bodyLength,
|
|
1885
1954
|
});
|
|
@@ -1894,20 +1963,55 @@ async function handleSend(req, res, ctx) {
|
|
|
1894
1963
|
* redaction rule): id, addresses, why-held reason, escalation flag, and enqueue time —
|
|
1895
1964
|
* the message BODY is deliberately never surfaced here (it travels only over the live
|
|
1896
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.
|
|
1897
1986
|
*/
|
|
1898
|
-
|
|
1899
|
-
|
|
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');
|
|
1900
1994
|
// Normalize to the stored realpath key (mailbox workspace_path is normalized at
|
|
1901
1995
|
// enqueue — tower-routes handleSend / holdAndRespond — matching overview.ts). Without
|
|
1902
1996
|
// this a symlinked workspace root would miss its own held rows.
|
|
1903
|
-
|
|
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);
|
|
1904
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.
|
|
1905
2006
|
const projected = rows.map((r) => ({
|
|
1906
2007
|
id: r.id,
|
|
1907
2008
|
workspacePath: r.workspace_path,
|
|
1908
2009
|
toAgent: r.to_agent,
|
|
1909
2010
|
fromAgent: r.from_agent,
|
|
1910
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,
|
|
1911
2015
|
escalated: r.escalated === 1,
|
|
1912
2016
|
createdAt: r.created_at,
|
|
1913
2017
|
// Spec 1313 round 3: due time of a pre-due delayed (`--delay`) row; null = deliver-ASAP.
|
|
@@ -1973,6 +2077,7 @@ function handleInboxShow(req, res, match) {
|
|
|
1973
2077
|
fromWorkspace: row.from_workspace,
|
|
1974
2078
|
status: row.status,
|
|
1975
2079
|
reason: row.reason,
|
|
2080
|
+
detail: row.detail, // Issue #1482: the gate verdict behind a `busy` hold
|
|
1976
2081
|
escalated: row.escalated === 1,
|
|
1977
2082
|
body: row.body,
|
|
1978
2083
|
createdAt: row.created_at,
|
|
@@ -2392,6 +2497,19 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
|
|
|
2392
2497
|
if (req.method === 'GET' && apiPath === 'overview') {
|
|
2393
2498
|
return handleOverview(res, url, workspacePath, ctx);
|
|
2394
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
|
+
}
|
|
2395
2513
|
// POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
|
|
2396
2514
|
if (req.method === 'POST' && apiPath === 'overview/refresh') {
|
|
2397
2515
|
return handleOverviewRefresh(res, ctx);
|
|
@@ -2612,7 +2730,9 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
2612
2730
|
}
|
|
2613
2731
|
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
2614
2732
|
entry.shells.set(shellId, session.id);
|
|
2615
|
-
|
|
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);
|
|
2616
2736
|
shellCreated = true;
|
|
2617
2737
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2618
2738
|
res.end(JSON.stringify({
|