@bridge4dev/runner 0.27.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude.js +391 -16
- package/dist/adapters/codex.js +187 -6
- package/dist/adapters/types.d.ts +115 -4
- package/dist/adapters/types.js +31 -0
- package/dist/attachments.d.ts +27 -0
- package/dist/attachments.js +150 -8
- package/dist/checkpoints.d.ts +175 -0
- package/dist/checkpoints.js +816 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +17 -0
- package/dist/index.js +30 -0
- package/dist/journal.d.ts +34 -1
- package/dist/journal.js +51 -2
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +12 -0
- package/dist/policy.d.ts +40 -0
- package/dist/policy.js +60 -6
- package/dist/protocol.d.ts +5 -5
- package/dist/supervisor.d.ts +90 -0
- package/dist/supervisor.js +692 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/codex.js
CHANGED
|
@@ -5,7 +5,7 @@ import { RUNNER_VERSION } from '../version.js';
|
|
|
5
5
|
import { repairCodexAuth } from './codex-home.js';
|
|
6
6
|
import { AppServerClient, asRecord, num, str } from './codex-protocol.js';
|
|
7
7
|
import { truncate } from './claude.js';
|
|
8
|
-
import {
|
|
8
|
+
import { availableModes, MODE_REFUSED_TEXT, MODE_WITHDRAWN_TEXT, } from './types.js';
|
|
9
9
|
import { answerSummary, invalidationMessage, mirrorOptions, newAskId, MAX_OPTIONS, MAX_QUESTIONS, OPTION_TEXT_LIMIT, QUESTION_TEXT_LIMIT, } from './questions.js';
|
|
10
10
|
// Codex adapter over `codex app-server` (stage C). The normalized AgentEvent
|
|
11
11
|
// contract is unchanged, so the dashboard renders Codex sessions with the same
|
|
@@ -135,10 +135,26 @@ class CodexSession {
|
|
|
135
135
|
threadId = null;
|
|
136
136
|
threadModel = null;
|
|
137
137
|
activeTurnId = null;
|
|
138
|
+
/**
|
|
139
|
+
* Last turn this thread finished (ticket #126).
|
|
140
|
+
*
|
|
141
|
+
* `thread/fork { lastTurnId }` forks THROUGH this turn inclusive, dropping
|
|
142
|
+
* everything after it — which is exactly "rewind the conversation to here".
|
|
143
|
+
* Verified against the JSON schema the installed codex-cli 0.145.0 generates
|
|
144
|
+
* itself (`codex app-server generate-json-schema`): `lastTurnId` is part of
|
|
145
|
+
* the STABLE surface, not the experimental one.
|
|
146
|
+
*
|
|
147
|
+
* `thread/rollback` is deliberately not used: its own schema marks it
|
|
148
|
+
* "DEPRECATED: will be removed soon", it counts turns from the end rather
|
|
149
|
+
* than naming one, and it mutates the thread in place instead of forking.
|
|
150
|
+
*/
|
|
151
|
+
lastCompletedTurnId = null;
|
|
138
152
|
/** Plan card waiting for the user; approving it starts the real work. */
|
|
139
153
|
heldPlan = null;
|
|
140
154
|
lastCollabMode = null;
|
|
141
155
|
mode;
|
|
156
|
+
/** A launch-time `full` this STRICT workspace does not allow (ticket #156). */
|
|
157
|
+
modeRefusedAtLaunch = false;
|
|
142
158
|
model;
|
|
143
159
|
effort;
|
|
144
160
|
/** Last model catalogue from `model/list` — effort sets differ per model. */
|
|
@@ -150,7 +166,13 @@ class CodexSession {
|
|
|
150
166
|
constructor(spec, home, deps) {
|
|
151
167
|
this.spec = spec;
|
|
152
168
|
this.home = home;
|
|
153
|
-
|
|
169
|
+
// `full` on a STRICT workspace is refused, not honoured: Codex in that mode
|
|
170
|
+
// sends no approvals at all, so layer 1 never runs and the manager's shield
|
|
171
|
+
// would be spent from the chat (ticket #156).
|
|
172
|
+
const requestedMode = spec.mode ?? 'ask';
|
|
173
|
+
this.mode = availableModes(spec.trustMode).includes(requestedMode) ? requestedMode : 'ask';
|
|
174
|
+
if (this.mode !== requestedMode)
|
|
175
|
+
this.modeRefusedAtLaunch = true;
|
|
154
176
|
this.model = spec.model;
|
|
155
177
|
this.effort = spec.effort;
|
|
156
178
|
this.repairHome = deps.repairHome ?? (deps.codexHome ? null : repairCodexAuth);
|
|
@@ -169,6 +191,10 @@ class CodexSession {
|
|
|
169
191
|
cwd: spec.cwd,
|
|
170
192
|
...wiring,
|
|
171
193
|
});
|
|
194
|
+
if (this.modeRefusedAtLaunch) {
|
|
195
|
+
this.notice('warn', MODE_REFUSED_TEXT);
|
|
196
|
+
this.emit({ type: 'settings', mode: this.mode });
|
|
197
|
+
}
|
|
172
198
|
void this.boot();
|
|
173
199
|
}
|
|
174
200
|
// ─── Boot ──────────────────────────────────────────────────────────
|
|
@@ -256,6 +282,19 @@ class CodexSession {
|
|
|
256
282
|
* second; losing the conversation costs the whole context.
|
|
257
283
|
*/
|
|
258
284
|
static RESUME_RETRY_DELAY_MS = 1_500;
|
|
285
|
+
/** Branch the thread at `lastTurnId`, dropping every later turn. */
|
|
286
|
+
async forkThread(threadId, lastTurnId) {
|
|
287
|
+
const result = asRecord(await this.client.request('thread/fork', {
|
|
288
|
+
threadId,
|
|
289
|
+
lastTurnId,
|
|
290
|
+
...this.threadParams(),
|
|
291
|
+
}));
|
|
292
|
+
const thread = asRecord(result['thread']);
|
|
293
|
+
const id = str(thread['id']);
|
|
294
|
+
if (!id)
|
|
295
|
+
throw new Error('thread/fork returned no thread id');
|
|
296
|
+
return { id, model: str(result['model']) ?? null };
|
|
297
|
+
}
|
|
259
298
|
async resumeThread(resumeId) {
|
|
260
299
|
// Resume takes the SAME overrides as start, and it must: the per-thread MCP
|
|
261
300
|
// overlay lives only in memory, so resuming with just a thread id brings the
|
|
@@ -273,6 +312,34 @@ class CodexSession {
|
|
|
273
312
|
}
|
|
274
313
|
async openThread() {
|
|
275
314
|
const resumeId = this.spec.resumeProviderSessionId;
|
|
315
|
+
// Ticket #126, conversation rewind. Forking rather than resuming: the
|
|
316
|
+
// thread the user rewound away from stays on disk untouched, and the fork
|
|
317
|
+
// carries the same overrides every other entry point passes (the per-thread
|
|
318
|
+
// MCP overlay lives only in memory, so a thread opened without it comes
|
|
319
|
+
// back with no DevBridge access at all).
|
|
320
|
+
if (resumeId && this.spec.resumeAtAnchor) {
|
|
321
|
+
try {
|
|
322
|
+
return await this.forkThread(resumeId, this.spec.resumeAtAnchor);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
log.warn('codex: thread/fork failed — resuming the whole conversation', {
|
|
326
|
+
sessionId: this.spec.sessionId,
|
|
327
|
+
error: describe(error),
|
|
328
|
+
});
|
|
329
|
+
// Reported through the SAME channel as Claude's refused resume, so the
|
|
330
|
+
// supervisor withdraws the pending feed cut here too. A notice alone
|
|
331
|
+
// left the page believing a rewind had happened: it had already thrown
|
|
332
|
+
// away messages this thread still holds.
|
|
333
|
+
this.emit({
|
|
334
|
+
type: 'error',
|
|
335
|
+
message: 'The conversation could not be rewound to that point — the agent still remembers everything after it',
|
|
336
|
+
code: 'rewind_failed',
|
|
337
|
+
// This thread carries on below with a plain resume — there is
|
|
338
|
+
// nothing for the supervisor to relaunch.
|
|
339
|
+
recovered: true,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
276
343
|
if (resumeId) {
|
|
277
344
|
try {
|
|
278
345
|
return await this.resumeThread(resumeId);
|
|
@@ -535,10 +602,28 @@ class CodexSession {
|
|
|
535
602
|
* tool call, so the next one already sees it.
|
|
536
603
|
*/
|
|
537
604
|
setWorkspacePolicy(policy) {
|
|
605
|
+
const trustChanged = policy.trustMode !== undefined && policy.trustMode !== this.spec.trustMode;
|
|
538
606
|
if (policy.trustMode !== undefined)
|
|
539
607
|
this.spec.trustMode = policy.trustMode;
|
|
540
608
|
if (policy.agentAutoCommit !== undefined)
|
|
541
609
|
this.spec.agentAutoCommit = policy.agentAutoCommit;
|
|
610
|
+
if (!trustChanged)
|
|
611
|
+
return;
|
|
612
|
+
// A manager tightened the project out from under a session that is running
|
|
613
|
+
// with nothing gated (QA-128). Codex carries `approvalPolicy` on the next
|
|
614
|
+
// `turn/start`, so recording the mode here IS the fix — the next turn is
|
|
615
|
+
// `on-request` and layer 1 sees every command again. Claude does the same
|
|
616
|
+
// thing through `setPermissionMode`; leaving this out of one adapter left
|
|
617
|
+
// the picker showing a mode that was no longer in its own list.
|
|
618
|
+
if (this.mode === 'full' && !availableModes(this.spec.trustMode).includes('full')) {
|
|
619
|
+
this.mode = 'ask';
|
|
620
|
+
this.notice('warn', MODE_WITHDRAWN_TEXT);
|
|
621
|
+
this.emit({ type: 'settings', mode: this.mode });
|
|
622
|
+
}
|
|
623
|
+
// `full` leaves (or rejoins) the picker with the trust level — the list is
|
|
624
|
+
// computed from `spec` and only travels in a capabilities frame (#156).
|
|
625
|
+
this.refreshCapabilities();
|
|
626
|
+
this.releaseApprovalsAllowedNow();
|
|
542
627
|
}
|
|
543
628
|
async setEffort(effort) {
|
|
544
629
|
if (effort)
|
|
@@ -548,9 +633,56 @@ class CodexSession {
|
|
|
548
633
|
this.emit({ type: 'settings', effort });
|
|
549
634
|
this.refreshCapabilities();
|
|
550
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* Codex needs no new process for any mode: `approvalPolicy` and the sandbox
|
|
638
|
+
* policy travel with the next `turn/start`, so the change is in force from
|
|
639
|
+
* the next turn whatever it is (ticket #156).
|
|
640
|
+
*/
|
|
641
|
+
modeSwitchNeedsRelaunch(_mode) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
551
644
|
async setMode(mode) {
|
|
645
|
+
if (!availableModes(this.spec.trustMode).includes(mode)) {
|
|
646
|
+
this.notice('warn', MODE_REFUSED_TEXT);
|
|
647
|
+
this.emit({ type: 'settings', mode: this.mode });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
552
650
|
this.mode = mode;
|
|
553
651
|
this.emit({ type: 'settings', mode });
|
|
652
|
+
// Ticket #157 — the card the user is looking at is the reason they reached
|
|
653
|
+
// for the control; judging only the next one leaves it sitting there.
|
|
654
|
+
this.releaseApprovalsAllowedNow();
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Re-judge every open approval under the rules in force NOW and accept the
|
|
658
|
+
* ones that no longer need a human (ticket #157). Permissive direction only:
|
|
659
|
+
* a card that would now be denied is left for the user, because taking a
|
|
660
|
+
* decision away from them is the mistake session 12 exists to undo.
|
|
661
|
+
*/
|
|
662
|
+
releaseApprovalsAllowedNow() {
|
|
663
|
+
for (const [requestId, pending] of [...this.approvals]) {
|
|
664
|
+
if (!pending.policy)
|
|
665
|
+
continue;
|
|
666
|
+
const verdict = evaluateToolUse(pending.policy.tool, pending.policy.input, {
|
|
667
|
+
trustMode: this.spec.trustMode,
|
|
668
|
+
mode: this.mode,
|
|
669
|
+
...(this.spec.agentAutoCommit === undefined
|
|
670
|
+
? {}
|
|
671
|
+
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
672
|
+
worktreePath: this.spec.cwd,
|
|
673
|
+
});
|
|
674
|
+
if (verdict.decision !== 'allow')
|
|
675
|
+
continue;
|
|
676
|
+
this.approvals.delete(requestId);
|
|
677
|
+
this.client.respond(pending.rpcId, { decision: 'accept' });
|
|
678
|
+
this.emit({
|
|
679
|
+
type: 'permission_resolved',
|
|
680
|
+
requestId,
|
|
681
|
+
allow: true,
|
|
682
|
+
source: 'policy',
|
|
683
|
+
reason: `the session mode changed — ${verdict.reason}`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
554
686
|
}
|
|
555
687
|
async interrupt() {
|
|
556
688
|
if (!this.threadId || !this.activeTurnId)
|
|
@@ -567,6 +699,33 @@ class CodexSession {
|
|
|
567
699
|
log.warn('codex: interrupt failed', { error: describe(error) });
|
|
568
700
|
}
|
|
569
701
|
}
|
|
702
|
+
conversationAnchor() {
|
|
703
|
+
return this.lastCompletedTurnId;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* `thread/compact/start` — a stable method of app-server 0.145.0, taking only
|
|
707
|
+
* the thread id and answering with an empty object; the summary itself
|
|
708
|
+
* arrives later as a `contextCompaction` item, which this adapter already
|
|
709
|
+
* turns into a notice.
|
|
710
|
+
*
|
|
711
|
+
* Until now compaction on this agent was entirely the agent's own business
|
|
712
|
+
* and the dashboard hid `/compact` from Codex sessions on the grounds that
|
|
713
|
+
* "that agent does not have it". It does.
|
|
714
|
+
*/
|
|
715
|
+
async compact() {
|
|
716
|
+
if (!this.threadId || this.stopped)
|
|
717
|
+
return false;
|
|
718
|
+
if (this.activeTurnId)
|
|
719
|
+
return false;
|
|
720
|
+
try {
|
|
721
|
+
await this.client.request('thread/compact/start', { threadId: this.threadId });
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
log.warn('codex: compaction refused', { error: describe(error) });
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
570
729
|
stop(reason = 'session_stopped') {
|
|
571
730
|
if (this.stopped)
|
|
572
731
|
return;
|
|
@@ -623,6 +782,9 @@ class CodexSession {
|
|
|
623
782
|
? { decision: 'ask', reason: 'details unavailable' }
|
|
624
783
|
: evaluateToolUse(enriched.policyTool, enriched.policyInput, {
|
|
625
784
|
trustMode: this.spec.trustMode,
|
|
785
|
+
// Ticket #156, the same missing argument as in the Claude adapter —
|
|
786
|
+
// both bridges call one policy, so both have to hand it the mode.
|
|
787
|
+
mode: this.mode,
|
|
626
788
|
...(this.spec.agentAutoCommit === undefined
|
|
627
789
|
? {}
|
|
628
790
|
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
@@ -643,7 +805,11 @@ class CodexSession {
|
|
|
643
805
|
});
|
|
644
806
|
return;
|
|
645
807
|
}
|
|
646
|
-
this.approvals.set(requestId, {
|
|
808
|
+
this.approvals.set(requestId, {
|
|
809
|
+
rpcId: request.id,
|
|
810
|
+
toolName: enriched.toolName,
|
|
811
|
+
policy: enriched.forceAsk ? null : { tool: enriched.policyTool, input: enriched.policyInput },
|
|
812
|
+
});
|
|
647
813
|
this.emit({
|
|
648
814
|
type: 'permission',
|
|
649
815
|
requestId,
|
|
@@ -1140,6 +1306,12 @@ class CodexSession {
|
|
|
1140
1306
|
onTurnCompleted(params) {
|
|
1141
1307
|
const turn = asRecord(params['turn']);
|
|
1142
1308
|
const status = str(turn['status']);
|
|
1309
|
+
// Ticket #126: the anchor a conversation rewind is measured from. Only a
|
|
1310
|
+
// COMPLETED turn may be one — `thread/fork` refuses a turn that is still in
|
|
1311
|
+
// progress, and the id is taken from the notification rather than from
|
|
1312
|
+
// `activeTurnId` so a steered turn anchors on what the thread actually
|
|
1313
|
+
// recorded.
|
|
1314
|
+
this.lastCompletedTurnId = str(turn['id']) ?? this.activeTurnId ?? this.lastCompletedTurnId;
|
|
1143
1315
|
this.activeTurnId = null;
|
|
1144
1316
|
// A held plan means the turn ended by proposing, not by finishing the work.
|
|
1145
1317
|
if (this.heldPlan)
|
|
@@ -1153,8 +1325,14 @@ class CodexSession {
|
|
|
1153
1325
|
});
|
|
1154
1326
|
return;
|
|
1155
1327
|
}
|
|
1156
|
-
// `interrupted` is a user-initiated Stop: the session stays usable.
|
|
1157
|
-
|
|
1328
|
+
// `interrupted` is a user-initiated Stop: the session stays usable. Named
|
|
1329
|
+
// rather than merely tolerated, so the feed can tell "stopped" from
|
|
1330
|
+
// "finished" — the same distinction the Claude adapter now draws.
|
|
1331
|
+
this.emit({
|
|
1332
|
+
type: 'turn_end',
|
|
1333
|
+
ok: true,
|
|
1334
|
+
...(status === 'interrupted' ? { aborted: true } : {}),
|
|
1335
|
+
});
|
|
1158
1336
|
this.flushQueued();
|
|
1159
1337
|
}
|
|
1160
1338
|
onStderr(text) {
|
|
@@ -1201,7 +1379,10 @@ class CodexSession {
|
|
|
1201
1379
|
const currentEffort = this.effort ?? models.find((m) => m.id === currentModel)?.defaultEffort ?? undefined;
|
|
1202
1380
|
const capabilities = {
|
|
1203
1381
|
models,
|
|
1204
|
-
|
|
1382
|
+
// Recomputed on every publication rather than captured: the workspace's
|
|
1383
|
+
// trust level changes under a running session, and `full` has to leave
|
|
1384
|
+
// the picker with it (ticket #156).
|
|
1385
|
+
modes: availableModes(this.spec.trustMode),
|
|
1205
1386
|
commands,
|
|
1206
1387
|
currentMode: this.mode,
|
|
1207
1388
|
...(currentModel ? { currentModel } : {}),
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -5,14 +5,48 @@ export interface McpConfig {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Agent-agnostic interaction mode (session-5 plan §2).
|
|
8
|
-
* ask — ask before acting (Claude `default`,
|
|
9
|
-
* plan — plan first, act after approval (Claude `plan`,
|
|
10
|
-
* auto —
|
|
8
|
+
* ask — ask before acting (Claude `default`, Codex `on-request`)
|
|
9
|
+
* plan — plan first, act after approval (Claude `plan`, Codex on-request + plan)
|
|
10
|
+
* auto — work on its own, hard limits only (Claude `auto`, Codex on-request + workspace-write)
|
|
11
11
|
* full — never ask (Claude `bypassPermissions`, Codex `never`)
|
|
12
|
+
*
|
|
13
|
+
* The mode is BOTH halves of the decision, and that took a bug to learn
|
|
14
|
+
* (ticket #156): it sets the agent CLI's own permission mode, AND it reaches
|
|
15
|
+
* `evaluateToolUse` as `PolicyContext.mode`. For four sessions it did only the
|
|
16
|
+
* first, so «Auto» moved a dial the layer that raises the cards never read.
|
|
12
17
|
*/
|
|
13
18
|
export type AgentMode = 'ask' | 'plan' | 'auto' | 'full';
|
|
14
19
|
export declare const AGENT_MODES: readonly AgentMode[];
|
|
15
20
|
export declare function isAgentMode(value: unknown): value is AgentMode;
|
|
21
|
+
/**
|
|
22
|
+
* Said in the feed whenever `full` is asked for on a STRICT workspace — at
|
|
23
|
+
* launch and on a live switch. One string, because it is one refusal, and the
|
|
24
|
+
* user should not have to notice that they arrived at it two different ways.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MODE_REFUSED_TEXT = "\u00ABUnrestricted\u00BB is not available on this project. Its trust level is Strict, and \u00ABUnrestricted\u00BB is the one mode in which DevBridge checks nothing \u2014 the two cannot both be true. This session asks about every command instead. Only a project manager can change the trust level, in the server panel.";
|
|
27
|
+
/**
|
|
28
|
+
* The other half of the same rule, and a different sentence on purpose
|
|
29
|
+
* (QA-128): this one is shown when the mode was taken AWAY from a running
|
|
30
|
+
* session because a manager tightened the project, not because the user asked
|
|
31
|
+
* for something they may not have. Telling somebody off for a decision that was
|
|
32
|
+
* not theirs is how a correct refusal still reads as a bug.
|
|
33
|
+
*/
|
|
34
|
+
export declare const MODE_WITHDRAWN_TEXT = "This project was just set to Strict trust, so \u00ABUnrestricted\u00BB is no longer available and this session now asks about every command. Nothing you did caused this \u2014 a project manager changed it in the server panel.";
|
|
35
|
+
/**
|
|
36
|
+
* The modes a session on THIS workspace may actually be put into (ticket #156).
|
|
37
|
+
*
|
|
38
|
+
* One entry differs from `AGENT_MODES`, and it is load-bearing: on a `STRICT`
|
|
39
|
+
* workspace `full` is not offered at all.
|
|
40
|
+
*
|
|
41
|
+
* `full` is the one mode in which layer 1 does not run — the agent CLI stops
|
|
42
|
+
* asking, so `evaluateToolUse` is never called and there is nothing left to
|
|
43
|
+
* enforce. On every other workspace that is the owner's explicit choice
|
|
44
|
+
* (2026-07-24: «full restricts nothing»). On a `STRICT` one it would be the
|
|
45
|
+
* opposite: a shield the manager set, spent from the chat by whoever opened the
|
|
46
|
+
* session. So the mode is removed from the list the dashboard draws its picker
|
|
47
|
+
* from, and refused if it arrives anyway.
|
|
48
|
+
*/
|
|
49
|
+
export declare function availableModes(trustMode: TrustMode): AgentMode[];
|
|
16
50
|
/**
|
|
17
51
|
* A reasoning-effort level a model supports. Codex advertises these per model
|
|
18
52
|
* (`supportedReasoningEfforts`) and the set differs between models — the new
|
|
@@ -146,6 +180,21 @@ export interface SessionSpec {
|
|
|
146
180
|
model?: string;
|
|
147
181
|
effort?: string;
|
|
148
182
|
resumeProviderSessionId?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Resume the conversation only up to this anchor, dropping everything after
|
|
185
|
+
* it (ticket #126, conversation rewind).
|
|
186
|
+
*
|
|
187
|
+
* The value is whatever `AgentSession.conversationAnchor()` returned earlier
|
|
188
|
+
* in this session's life — a Claude message uuid or a Codex turn id. It is
|
|
189
|
+
* opaque to everything above the adapter, and it is only ever meaningful
|
|
190
|
+
* together with `resumeProviderSessionId`.
|
|
191
|
+
*
|
|
192
|
+
* Both agents answer this by FORKING rather than by truncating in place, so
|
|
193
|
+
* the original conversation is never destroyed: the adapter reports the new
|
|
194
|
+
* provider session id through `provider_session`, and the old one stays on
|
|
195
|
+
* disk exactly as it was.
|
|
196
|
+
*/
|
|
197
|
+
resumeAtAnchor?: string;
|
|
149
198
|
mcp?: McpConfig;
|
|
150
199
|
maxBudgetUsd?: number;
|
|
151
200
|
}
|
|
@@ -313,6 +362,14 @@ export type AgentEvent = {
|
|
|
313
362
|
type: 'turn_end';
|
|
314
363
|
ok: boolean;
|
|
315
364
|
errorMessage?: string;
|
|
365
|
+
/**
|
|
366
|
+
* The turn ended because the user pressed Stop, not because it finished.
|
|
367
|
+
*
|
|
368
|
+
* Reported `ok: true` — a stop is not a failure — but named, so the feed
|
|
369
|
+
* can say «stopped» rather than «done» and so a stopped turn never reads
|
|
370
|
+
* as work the agent completed.
|
|
371
|
+
*/
|
|
372
|
+
aborted?: boolean;
|
|
316
373
|
} | {
|
|
317
374
|
type: 'error';
|
|
318
375
|
message: string;
|
|
@@ -323,7 +380,23 @@ export type AgentEvent = {
|
|
|
323
380
|
* never signed in "expired" sent people looking for a problem that did
|
|
324
381
|
* not exist.
|
|
325
382
|
*/
|
|
326
|
-
|
|
383
|
+
/**
|
|
384
|
+
* `rewind_failed` — the CLI refused the point we asked it to resume at
|
|
385
|
+
* (ticket #126). Recoverable and NOT a failed session: the conversation
|
|
386
|
+
* is intact, only the rewind did not happen, and the caller must say so
|
|
387
|
+
* rather than pretending it did.
|
|
388
|
+
*/
|
|
389
|
+
code?: 'resume_failed' | 'auth_expired' | 'auth_missing' | 'rewind_failed';
|
|
390
|
+
/**
|
|
391
|
+
* The adapter has already dealt with it and the session is still running.
|
|
392
|
+
*
|
|
393
|
+
* Only meaningful with `rewind_failed`, and it is the difference between
|
|
394
|
+
* the two agents: Claude's refused resume kills the query, so the
|
|
395
|
+
* supervisor must bring the process back without the anchor; Codex falls
|
|
396
|
+
* back to a plain resume inside `openThread` and needs no relaunch at
|
|
397
|
+
* all. Both still have to withdraw the feed cut and say what happened.
|
|
398
|
+
*/
|
|
399
|
+
recovered?: boolean;
|
|
327
400
|
};
|
|
328
401
|
export interface AgentSession {
|
|
329
402
|
/** Ends when the underlying agent process is gone. */
|
|
@@ -349,6 +422,24 @@ export interface AgentSession {
|
|
|
349
422
|
setEffort(effort: string | null): Promise<void>;
|
|
350
423
|
/** Switch interaction mode mid-session. */
|
|
351
424
|
setMode(mode: AgentMode): Promise<void>;
|
|
425
|
+
/**
|
|
426
|
+
* Can this session reach `mode` without a new agent process? (ticket #156)
|
|
427
|
+
*
|
|
428
|
+
* Claude answers `false` for any move across the `full` boundary, and that is
|
|
429
|
+
* the CLI's rule rather than ours: `bypassPermissions` may only be entered by
|
|
430
|
+
* a process LAUNCHED with the dangerous flag, and the control request is
|
|
431
|
+
* refused outright otherwise —
|
|
432
|
+
*
|
|
433
|
+
* Cannot set permission mode to bypassPermissions because the session was
|
|
434
|
+
* not launched with --dangerously-skip-permissions
|
|
435
|
+
*
|
|
436
|
+
* (verified live against the bundled CLI 2.1.218). Until this existed the
|
|
437
|
+
* supervisor caught that error, turned it into a `notice`, and left the user
|
|
438
|
+
* with a picker reading «Unrestricted» over a session that was still asking.
|
|
439
|
+
*
|
|
440
|
+
* Codex answers `true` always: its policy travels with the next `turn/start`.
|
|
441
|
+
*/
|
|
442
|
+
modeSwitchNeedsRelaunch(mode: AgentMode): boolean;
|
|
352
443
|
/**
|
|
353
444
|
* Project-level policy changed while this session is running (session 15).
|
|
354
445
|
*
|
|
@@ -365,6 +456,26 @@ export interface AgentSession {
|
|
|
365
456
|
}): void;
|
|
366
457
|
/** Interrupt the current turn (session stays resumable). */
|
|
367
458
|
interrupt(): Promise<void>;
|
|
459
|
+
/**
|
|
460
|
+
* An opaque id naming the conversation as it stands RIGHT NOW (ticket #126).
|
|
461
|
+
*
|
|
462
|
+
* Handed back later as `SessionSpec.resumeAtAnchor` to rewind the agent's
|
|
463
|
+
* memory to this exact point. `null` means the agent has nothing to anchor to
|
|
464
|
+
* yet — a session whose first turn has not finished — and the caller must
|
|
465
|
+
* then treat "rewind to here" as "start the conversation over".
|
|
466
|
+
*
|
|
467
|
+
* Claude: the uuid of the last message in its transcript.
|
|
468
|
+
* Codex: the id of the last completed turn.
|
|
469
|
+
*/
|
|
470
|
+
conversationAnchor(): string | null;
|
|
471
|
+
/**
|
|
472
|
+
* Ask the agent to summarise the conversation so far and continue from the
|
|
473
|
+
* summary — `/compact` in either CLI.
|
|
474
|
+
*
|
|
475
|
+
* Returns false when the agent is in no state to do it (mid-turn, or the
|
|
476
|
+
* call was refused); the caller says so rather than pretending it happened.
|
|
477
|
+
*/
|
|
478
|
+
compact(): Promise<boolean>;
|
|
368
479
|
/**
|
|
369
480
|
* Tear the session down (kills the agent process). The reason travels so
|
|
370
481
|
* anything the user was still being asked is withdrawn with a cause they can
|
package/dist/adapters/types.js
CHANGED
|
@@ -2,4 +2,35 @@ export const AGENT_MODES = ['ask', 'plan', 'auto', 'full'];
|
|
|
2
2
|
export function isAgentMode(value) {
|
|
3
3
|
return typeof value === 'string' && AGENT_MODES.includes(value);
|
|
4
4
|
}
|
|
5
|
+
/**
|
|
6
|
+
* Said in the feed whenever `full` is asked for on a STRICT workspace — at
|
|
7
|
+
* launch and on a live switch. One string, because it is one refusal, and the
|
|
8
|
+
* user should not have to notice that they arrived at it two different ways.
|
|
9
|
+
*/
|
|
10
|
+
export const MODE_REFUSED_TEXT = '«Unrestricted» is not available on this project. Its trust level is Strict, and «Unrestricted» is the one mode in which DevBridge checks nothing — the two cannot both be true. This session asks about every command instead. Only a project manager can change the trust level, in the server panel.';
|
|
11
|
+
/**
|
|
12
|
+
* The other half of the same rule, and a different sentence on purpose
|
|
13
|
+
* (QA-128): this one is shown when the mode was taken AWAY from a running
|
|
14
|
+
* session because a manager tightened the project, not because the user asked
|
|
15
|
+
* for something they may not have. Telling somebody off for a decision that was
|
|
16
|
+
* not theirs is how a correct refusal still reads as a bug.
|
|
17
|
+
*/
|
|
18
|
+
export const MODE_WITHDRAWN_TEXT = 'This project was just set to Strict trust, so «Unrestricted» is no longer available and this session now asks about every command. Nothing you did caused this — a project manager changed it in the server panel.';
|
|
19
|
+
/**
|
|
20
|
+
* The modes a session on THIS workspace may actually be put into (ticket #156).
|
|
21
|
+
*
|
|
22
|
+
* One entry differs from `AGENT_MODES`, and it is load-bearing: on a `STRICT`
|
|
23
|
+
* workspace `full` is not offered at all.
|
|
24
|
+
*
|
|
25
|
+
* `full` is the one mode in which layer 1 does not run — the agent CLI stops
|
|
26
|
+
* asking, so `evaluateToolUse` is never called and there is nothing left to
|
|
27
|
+
* enforce. On every other workspace that is the owner's explicit choice
|
|
28
|
+
* (2026-07-24: «full restricts nothing»). On a `STRICT` one it would be the
|
|
29
|
+
* opposite: a shield the manager set, spent from the chat by whoever opened the
|
|
30
|
+
* session. So the mode is removed from the list the dashboard draws its picker
|
|
31
|
+
* from, and refused if it arrives anyway.
|
|
32
|
+
*/
|
|
33
|
+
export function availableModes(trustMode) {
|
|
34
|
+
return trustMode === 'STRICT' ? AGENT_MODES.filter((mode) => mode !== 'full') : [...AGENT_MODES];
|
|
35
|
+
}
|
|
5
36
|
//# sourceMappingURL=types.js.map
|
package/dist/attachments.d.ts
CHANGED
|
@@ -67,6 +67,17 @@ export declare function saveAttachments(input: {
|
|
|
67
67
|
saved: SavedAttachment[];
|
|
68
68
|
failed: string[];
|
|
69
69
|
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Delete old attachments from a session worktree.
|
|
72
|
+
*
|
|
73
|
+
* These files are invisible to git by design, which also means nothing else
|
|
74
|
+
* will ever clean them up: a long-lived workspace would accumulate every
|
|
75
|
+
* screenshot and archive anyone attached to it, on the owner's own disk. Age
|
|
76
|
+
* first, then a size budget for the case where age alone is not enough.
|
|
77
|
+
*
|
|
78
|
+
* Best effort — a folder we cannot prune is not a reason to lose the message.
|
|
79
|
+
*/
|
|
80
|
+
export declare function pruneAttachmentDir(dir: string, now?: number): void;
|
|
70
81
|
/**
|
|
71
82
|
* Turn the user's message plus the files into one prompt.
|
|
72
83
|
*
|
|
@@ -74,6 +85,22 @@ export declare function saveAttachments(input: {
|
|
|
74
85
|
* files with their own tools, and an agent-specific encoding (image blocks for
|
|
75
86
|
* Claude, `localImage` items for Codex) would be two code paths that drift.
|
|
76
87
|
* The user's own words stay first — the files are context, not the request.
|
|
88
|
+
*
|
|
89
|
+
* Each line NAMES the file and stops there (#128). The old version ended every
|
|
90
|
+
* list with «Open them before answering — images included», which was false for
|
|
91
|
+
* a `.zip` — there is nothing to look at — and false for a `.docx`. An
|
|
92
|
+
* instruction that is wrong for the file in front of the agent is worse than no
|
|
93
|
+
* instruction: it produces a confident answer about a document nobody opened.
|
|
94
|
+
*
|
|
95
|
+
* What we deliberately do NOT do is decide for the agent. Unpacking an archive,
|
|
96
|
+
* or extracting a document's text and handing over our version of it, would put
|
|
97
|
+
* DevBridge in charge of a job the agent does better with the whole file in
|
|
98
|
+
* front of it — and would mean the agent answers about what WE chose to show,
|
|
99
|
+
* not about what the person actually attached.
|
|
100
|
+
*
|
|
101
|
+
* The closing line is a trust frame, not decoration. These files come from a
|
|
102
|
+
* person through a web form; anything inside one that reads like an order to
|
|
103
|
+
* the agent is data, not authority (react-security-standards AI.2/AI.3).
|
|
77
104
|
*/
|
|
78
105
|
export declare function composeMessageWithAttachments(text: string, saved: SavedAttachment[]): string;
|
|
79
106
|
//# sourceMappingURL=attachments.d.ts.map
|