@vibedgc/sdk 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1739 @@
1
+ /**
2
+ * One isolated DGC session: at most one active run (plus follow-ups queued behind it).
3
+ * Mirrors sdk/python/dgc_sdk/session.py.
4
+ */
5
+ import { basename } from "node:path";
6
+ import { randomUUID } from "node:crypto";
7
+ import { DGCCommandRejectedError, DGCConfigError, DGCError, DGCRuntimeError, DGCTimeoutError } from "./errors.js";
8
+ import { longTimer } from "./transport.js";
9
+ import { assertSupported, extractJson, validate as validateSchema } from "./schema.js";
10
+ import { USAGE_TOTAL_KEYS, costUsd, emptyTotals, reported, usageDelta, usageTotals } from "./usage.js";
11
+ import { diffWorkspace, snapshotWorkspace } from "./changes.js";
12
+ import { resolvePermission } from "./policy.js";
13
+ const LOOPBACK = /https?:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?\/\S+/gi;
14
+ // Control events left on the pipe after resume/fork/rewind; not attributed to the next run.
15
+ const IDLE_EVENT_TYPES = new Set([
16
+ "history", "agents", "context", "goal_changed", "monitors", "info", "todos",
17
+ "session", "session_named", "handoff_started", "ready", "config",
18
+ ]);
19
+ const TASK_MAP = {
20
+ done: "completed", completed: "completed", in_progress: "in_progress", progress: "in_progress",
21
+ blocked: "blocked", cancelled: "cancelled", canceled: "cancelled", pending: "pending", todo: "pending",
22
+ };
23
+ const TERMINAL = new Set(["completed", "cancelled", "failed"]);
24
+ const DECISION_EVENTS = new Set(["permission_request", "plan_proposal", "options_request", "mcp_input_request"]);
25
+ const AUDIT_EVENTS = new Set([
26
+ "turn_start", "turn_end", "tool_call", "tool_result", "tool_denied", "permission_request", "error",
27
+ ]);
28
+ const AUDIT_FIELDS = [
29
+ "name", "id", "reason", "summary", "path", "message", "is_error", "args", "output", "diff",
30
+ "command", "turn_id", "request_id", "call_id",
31
+ ];
32
+ const SLICE_MS = 500; // longest a pump waits on the pipe before re-checking deadlines
33
+ const CANCEL_GRACE_MS = 10_000; // after a cancel, how long to wait for the turn to end
34
+ const TIMEOUT_GRACE_MS = 5_000; // after a run timeout, how long to wait for the cancelled turn
35
+ const STEER_GRACE_MS = 5_000; // after the last turn, how long to wait for a steer's outcome
36
+ const BILLING_WAIT_MS = 2_000; // after the last turn, how long to wait for its usage totals
37
+ const VERIFY_STARTED = "⧗ verify:";
38
+ const DEFAULT_RUN_TIMEOUT_MS = 180_000;
39
+ function newId(prefix) {
40
+ return `${prefix}-${randomUUID().replace(/-/g, "").slice(0, 12)}`;
41
+ }
42
+ function warn(message) {
43
+ process.emitWarning(message, { code: "DGC_SDK" });
44
+ }
45
+ function sleep(ms) {
46
+ return new Promise((done) => setTimeout(done, ms));
47
+ }
48
+ function mapping(value) {
49
+ return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
50
+ }
51
+ function exitCode(output) {
52
+ const lowered = (output || "").toLowerCase();
53
+ const at = lowered.indexOf("exit code:");
54
+ if (at < 0)
55
+ return null;
56
+ const word = lowered.slice(at + "exit code:".length).trim().split(/\s+/)[0];
57
+ const code = Number.parseInt(word, 10);
58
+ return Number.isNaN(code) ? null : code;
59
+ }
60
+ function sameCommand(left, right) {
61
+ return String(left || "").split(/\s+/).filter(Boolean).join(" ") === String(right || "").split(/\s+/).filter(Boolean).join(" ");
62
+ }
63
+ function agentFromRow(row) {
64
+ return {
65
+ id: String(row.id || ""),
66
+ state: String(row.state || ""),
67
+ description: String(row.description || ""),
68
+ parentId: row.parent_id ? String(row.parent_id) : null,
69
+ };
70
+ }
71
+ /** Best-effort category for a `tool_denied` reason (see {@link Denial}). */
72
+ export function denialSource(reason) {
73
+ const low = (reason || "").toLowerCase();
74
+ if (low.includes("pretooluse hook") || low.includes("blocked by a pretooluse"))
75
+ return "hook";
76
+ if (low.includes("plan mode") || low.includes("monitor event") || low.includes("approve on your next prompt"))
77
+ return "mode";
78
+ if (low.includes("deny rule") || low.includes("session policy") || low.includes("outside the project")
79
+ || low.includes("application running this session") || low.includes("the application's"))
80
+ return "policy";
81
+ if (low.includes("denied by the user") || low.includes("the user denied") || low === "denied")
82
+ return "callback";
83
+ return "runtime";
84
+ }
85
+ function checkTimeout(timeoutMs, name = "timeoutMs") {
86
+ if (timeoutMs === undefined || timeoutMs === null)
87
+ return;
88
+ if (typeof timeoutMs !== "number" || !(timeoutMs > 0) || !Number.isFinite(timeoutMs)) {
89
+ throw new DGCConfigError(`${name} must be a positive number of milliseconds, or null for no limit`);
90
+ }
91
+ }
92
+ /** Pump-side state of one run. Outcome events for its prompt ids land here, whoever reads them. */
93
+ export class Run {
94
+ result;
95
+ requestId;
96
+ ids = new Set(); // prompt / steer / repair ids this run owns
97
+ expect = new Set(); // ids whose turn has not started yet
98
+ steers = new Set();
99
+ unresolved = new Set(); // steers without an outcome yet
100
+ returned = new Set(); // prompts DGC handed back unrun
101
+ rejected = new Map();
102
+ turnIds = new Set();
103
+ startedIds = new Set();
104
+ liveTurn = "";
105
+ started = false;
106
+ cancelReason = "";
107
+ cancelAt = 0;
108
+ cancelSent = false;
109
+ decisionError = "";
110
+ usage = emptyTotals();
111
+ billed = 0;
112
+ pendingBills = 0;
113
+ usageUnknown = false;
114
+ done = false;
115
+ handle = null;
116
+ pumpStarted = false;
117
+ holdsFollowups = false; // this run changes DGC's config for its own turns
118
+ held = false; // a follow-up the SDK sends once the run in front of it is over
119
+ /** The workspace when this run's last turn ended: its "after", and a follow-up's "before". */
120
+ after = null;
121
+ cancelWaiters = [];
122
+ doneWaiters = [];
123
+ constructor(result, requestId) {
124
+ this.result = result;
125
+ this.requestId = requestId;
126
+ this.ids.add(requestId);
127
+ this.expect.add(requestId);
128
+ }
129
+ /** Resolves once a cancel (of any reason) is requested. */
130
+ cancelled() {
131
+ if (this.cancelReason)
132
+ return Promise.resolve();
133
+ return new Promise((done) => this.cancelWaiters.push(done));
134
+ }
135
+ noteCancel() {
136
+ for (const wake of this.cancelWaiters.splice(0))
137
+ wake();
138
+ }
139
+ markDone() {
140
+ this.done = true;
141
+ for (const wake of this.doneWaiters.splice(0))
142
+ wake();
143
+ }
144
+ waitDone(timeoutMs) {
145
+ if (this.done)
146
+ return Promise.resolve();
147
+ return new Promise((done) => {
148
+ const timer = setTimeout(done, timeoutMs);
149
+ this.doneWaiters.push(() => { clearTimeout(timer); done(); });
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * Streaming handle. Iterate its events (`for await`), then call {@link result}. Leaving the loop
155
+ * early (`break`, `return`, a throw) cancels the run and waits (bounded) for DGC to stop, so the
156
+ * session is free again. `result()` drains whatever was not iterated.
157
+ */
158
+ export class RunHandle {
159
+ session;
160
+ /** @internal */
161
+ run;
162
+ gen = null;
163
+ primed = null;
164
+ chain = Promise.resolve();
165
+ finished = false;
166
+ detach = () => { };
167
+ constructor(session, run) {
168
+ this.session = session;
169
+ this.run = run;
170
+ run.handle = this;
171
+ }
172
+ get runId() {
173
+ return this.run.result.runId;
174
+ }
175
+ /** True once the run is over and every event was consumed. */
176
+ get done() {
177
+ return this.finished;
178
+ }
179
+ /**
180
+ * @internal Attach the pump. `prime` starts it now, so the prompt is sent when stream()
181
+ * returns; a run queued behind another starts when it is iterated or awaited (starting it here
182
+ * would drain the run in front of it behind its caller's back).
183
+ */
184
+ begin(gen, signal, prime) {
185
+ this.gen = gen;
186
+ if (signal) {
187
+ const abort = () => {
188
+ try {
189
+ this.cancel();
190
+ }
191
+ catch { /* the backend is gone; the run reports it */ }
192
+ };
193
+ if (signal.aborted)
194
+ this.run.cancelReason = "cancelled";
195
+ else {
196
+ signal.addEventListener("abort", abort, { once: true });
197
+ this.detach = () => signal.removeEventListener("abort", abort);
198
+ }
199
+ }
200
+ if (prime) {
201
+ this.primed = gen.next();
202
+ this.primed.catch(() => { });
203
+ }
204
+ }
205
+ step() {
206
+ const next = this.chain.then(async () => {
207
+ try {
208
+ if (this.primed) {
209
+ const primed = this.primed;
210
+ this.primed = null;
211
+ const first = await primed;
212
+ if (first.done) {
213
+ this.finish();
214
+ return null;
215
+ }
216
+ return first.value;
217
+ }
218
+ if (this.finished || !this.gen)
219
+ return null;
220
+ const item = await this.gen.next();
221
+ if (item.done) {
222
+ this.finish();
223
+ return null;
224
+ }
225
+ return item.value;
226
+ }
227
+ catch (error) {
228
+ this.finish();
229
+ throw error;
230
+ }
231
+ });
232
+ this.chain = next.catch(() => { });
233
+ return next;
234
+ }
235
+ finish() {
236
+ this.finished = true;
237
+ this.detach();
238
+ }
239
+ [Symbol.asyncIterator]() {
240
+ return {
241
+ next: async () => {
242
+ const event = await this.step();
243
+ return event ? { value: event, done: false } : { value: undefined, done: true };
244
+ },
245
+ return: async () => {
246
+ if (!this.finished)
247
+ await this.abandon();
248
+ return { value: undefined, done: true };
249
+ },
250
+ };
251
+ }
252
+ /**
253
+ * Drain the run and return its result. `timeoutMs` bounds this wait (undefined/null waits until
254
+ * the run ends, which the run's own timeout bounds); if the run is still going when it lapses,
255
+ * DGCTimeoutError is thrown and the run keeps going (call {@link cancel} to stop it).
256
+ */
257
+ async result(timeoutMs) {
258
+ const drain = (async () => {
259
+ while (await this.step()) { /* drain */ }
260
+ return this.run.result;
261
+ })();
262
+ if (timeoutMs === undefined || timeoutMs === null)
263
+ return drain;
264
+ checkTimeout(timeoutMs);
265
+ drain.catch(() => { });
266
+ let stop = () => { };
267
+ const lapse = new Promise((_resolve, reject) => {
268
+ stop = longTimer(timeoutMs, () => reject(new DGCTimeoutError(`run ${this.runId} was still ${this.run.result.status} after ${timeoutMs} ms`)));
269
+ });
270
+ try {
271
+ return await Promise.race([drain, lapse]);
272
+ }
273
+ finally {
274
+ stop();
275
+ }
276
+ }
277
+ /** Cancel this run. The result settles with `status: "cancelled"`. */
278
+ cancel() {
279
+ this.session.cancelRun(this.run, "cancelled");
280
+ }
281
+ /** Cancel if still going, then drain (bounded); used when the consumer went away. */
282
+ async abandon(timeoutMs = 15_000) {
283
+ if (!this.finished) {
284
+ try {
285
+ this.session.cancelRun(this.run, "cancelled");
286
+ }
287
+ catch { /* the transport is gone */ }
288
+ try {
289
+ await this.result(timeoutMs);
290
+ }
291
+ catch { /* the transport may already be gone */ }
292
+ }
293
+ if (!this.finished)
294
+ await this.close();
295
+ }
296
+ async close() {
297
+ const gen = this.gen;
298
+ this.finish();
299
+ if (gen) {
300
+ await Promise.race([gen.return(undefined).catch(() => undefined), sleep(5_000)]);
301
+ }
302
+ if (!this.run.pumpStarted)
303
+ this.session.forget(this.run);
304
+ }
305
+ }
306
+ export class Session {
307
+ sessionId;
308
+ sessionPath = "";
309
+ transport;
310
+ protocolVersion;
311
+ capabilities;
312
+ /** Whether this session's shell commands run inside the OS sandbox. */
313
+ sandbox;
314
+ init;
315
+ closed = false;
316
+ pending = [];
317
+ owners = new Map();
318
+ active = null;
319
+ queued = [];
320
+ turn = null;
321
+ usageLast = null;
322
+ billTo = null;
323
+ taskIds = new Map();
324
+ taskRevision = 0;
325
+ answeredIds = new Set();
326
+ maxTurnsDirty = false;
327
+ modelSeen;
328
+ constructor(transport, ready, init) {
329
+ if (init.unhandled !== "deny" && init.unhandled !== "callback") {
330
+ throw new DGCConfigError("permissions.unhandled must be 'deny' or 'callback'");
331
+ }
332
+ this.transport = transport;
333
+ this.init = init;
334
+ this.sessionId = String(ready.session_id || "");
335
+ this.protocolVersion = ready.protocol_version;
336
+ this.capabilities = mapping(ready.capabilities);
337
+ this.sandbox = init.sandbox;
338
+ this.modelSeen = String(ready.model || init.model || "");
339
+ }
340
+ /** Advanced transport. Prefer {@link run} / {@link stream}. */
341
+ get raw() {
342
+ return this.transport;
343
+ }
344
+ /** @internal The verify command, for the accumulator. */
345
+ get verifyCommand() {
346
+ return this.init.verifyCommand;
347
+ }
348
+ async close() {
349
+ if (this.closed)
350
+ return;
351
+ this.closed = true;
352
+ this.init.toolHub?.close();
353
+ await this.transport.close();
354
+ }
355
+ // ---- runs -----------------------------------------------------------------------------------
356
+ async run(prompt, runOptions) {
357
+ return this.stream(prompt, runOptions).result();
358
+ }
359
+ /**
360
+ * Send `prompt` and return a handle over its events. `timeoutMs` limits each turn of the run
361
+ * (default 180 000; null for none). `maxTurns` caps tool iterations for this run only; the
362
+ * session's own setting is restored afterwards. `signal` cancels the run when aborted.
363
+ */
364
+ stream(prompt, runOptions = {}) {
365
+ if (this.closed)
366
+ throw new DGCRuntimeError("this session is closed");
367
+ if (typeof prompt !== "string" || !prompt.trim())
368
+ throw new DGCConfigError("prompt must be a non-empty string");
369
+ if (runOptions.outputSchema)
370
+ assertSupported(runOptions.outputSchema);
371
+ checkTimeout(runOptions.timeoutMs);
372
+ const maxTurns = runOptions.maxTurns;
373
+ if (maxTurns !== undefined) {
374
+ if (typeof maxTurns !== "number" || !Number.isInteger(maxTurns) || maxTurns < 0) {
375
+ throw new DGCConfigError("maxTurns must be a non-negative integer");
376
+ }
377
+ if (!this.init.isolated) {
378
+ throw new DGCConfigError("a per-run maxTurns needs an isolated session: with inheritUserState DGC "
379
+ + "would save it into your own ~/.dgc/config.json");
380
+ }
381
+ }
382
+ const current = this.active;
383
+ if (current && !current.done && !current.cancelReason) {
384
+ throw new DGCConfigError("this session already has an active run");
385
+ }
386
+ // A cancelled run still winding down, or queued follow-ups: this run starts after them.
387
+ const prior = this.queued.length ? this.queued[this.queued.length - 1] : (current && !current.done ? current : null);
388
+ const run = new Run(this.newResult("running"), newId("req"));
389
+ run.holdsFollowups = maxTurns !== undefined || Boolean(runOptions.outputSchema);
390
+ if (prior)
391
+ this.queued.push(run);
392
+ else
393
+ this.active = run;
394
+ this.owners.set(run.requestId, run);
395
+ const handle = new RunHandle(this, run);
396
+ const timeoutMs = runOptions.timeoutMs === undefined ? DEFAULT_RUN_TIMEOUT_MS : runOptions.timeoutMs;
397
+ handle.begin(this.pump(run, prompt.trim(), timeoutMs, maxTurns, runOptions.outputSchema, runOptions.skills, runOptions.workflow, runOptions.repairAttempts ?? 1, true, prior), runOptions.signal, prior === null);
398
+ return handle;
399
+ }
400
+ /**
401
+ * Queue a prompt behind the active run and return a handle for its own turn. With no run in
402
+ * flight this is {@link stream}. A follow-up is observed, audited and billed like any run.
403
+ * Behind a run with its own `maxTurns` or `outputSchema`, it is sent when that run is over (so
404
+ * it runs under the session's own settings). It does not run when the run in front of it is
405
+ * cancelled or times out.
406
+ */
407
+ followup(text, runOptions = {}) {
408
+ if (typeof text !== "string" || !text.trim())
409
+ throw new DGCConfigError("followup text must be a non-empty string");
410
+ if (runOptions.outputSchema)
411
+ assertSupported(runOptions.outputSchema);
412
+ checkTimeout(runOptions.timeoutMs);
413
+ if (this.closed)
414
+ throw new DGCRuntimeError("this session is closed");
415
+ const prior = this.queued.length ? this.queued[this.queued.length - 1] : (this.active && !this.active.done ? this.active : null);
416
+ if (!prior)
417
+ return this.stream(text, runOptions);
418
+ const queued = new Run(this.newResult("queued"), newId("follow"));
419
+ queued.held = prior.holdsFollowups || prior.held;
420
+ queued.holdsFollowups = Boolean(runOptions.outputSchema);
421
+ this.queued.push(queued);
422
+ this.owners.set(queued.requestId, queued);
423
+ if (!queued.held) {
424
+ const payload = {
425
+ type: "prompt", text: this.composePrompt(text.trim()), delivery: "queue", request_id: queued.requestId,
426
+ };
427
+ if (runOptions.skills?.length)
428
+ payload.skills = [...runOptions.skills];
429
+ try {
430
+ this.transport.send(payload);
431
+ }
432
+ catch (error) {
433
+ this.forget(queued);
434
+ throw new DGCRuntimeError(error instanceof Error ? error.message : String(error), { cause: error });
435
+ }
436
+ }
437
+ const handle = new RunHandle(this, queued);
438
+ const timeoutMs = runOptions.timeoutMs === undefined ? DEFAULT_RUN_TIMEOUT_MS : runOptions.timeoutMs;
439
+ handle.begin(this.pump(queued, text.trim(), timeoutMs, undefined, runOptions.outputSchema, runOptions.skills, undefined, runOptions.repairAttempts ?? 1, queued.held, prior), runOptions.signal, false);
440
+ return handle;
441
+ }
442
+ /** Cancel the active run. DGC's stop also hands back prompts queued behind it. */
443
+ cancel() {
444
+ const run = this.active;
445
+ if (!run) {
446
+ try {
447
+ this.transport.send({ type: "cancel" });
448
+ }
449
+ catch (error) {
450
+ throw new DGCRuntimeError("could not cancel the run", { cause: error });
451
+ }
452
+ return;
453
+ }
454
+ this.cancelRun(run, "cancelled");
455
+ }
456
+ /**
457
+ * Add `text` to the run in flight. Throws unless a run is active. If DGC cannot fold it into
458
+ * the live turn, it runs as a further turn of the same run (its tool calls are part of that
459
+ * run's result, audit and usage).
460
+ */
461
+ steer(text) {
462
+ if (typeof text !== "string" || !text.trim())
463
+ throw new DGCConfigError("steer text must be a non-empty string");
464
+ const run = this.active;
465
+ if (!run || run.done || run.cancelReason) {
466
+ throw new DGCConfigError("steer() needs an active run; use followup() or stream()");
467
+ }
468
+ const rid = newId("steer");
469
+ run.ids.add(rid);
470
+ run.steers.add(rid);
471
+ run.unresolved.add(rid);
472
+ this.owners.set(rid, run);
473
+ try {
474
+ this.transport.send({ type: "prompt", text: text.trim(), delivery: "steer", request_id: rid });
475
+ }
476
+ catch (error) {
477
+ run.unresolved.delete(rid);
478
+ throw new DGCRuntimeError(error instanceof Error ? error.message : String(error), { cause: error });
479
+ }
480
+ }
481
+ /**
482
+ * Fill `sessionPath` once this session's own transcript exists. Only a transcript whose file
483
+ * name is this session's id is used; another session's transcript is never adopted.
484
+ */
485
+ async bindIdentity() {
486
+ if (!this.sessionId)
487
+ return;
488
+ for (const info of await this.listSessions()) {
489
+ if (info.id === this.sessionId) {
490
+ this.sessionPath = info.path;
491
+ return;
492
+ }
493
+ }
494
+ }
495
+ newResult(status) {
496
+ return {
497
+ sessionId: this.sessionId, runId: newId("run"), status, reason: "", finalText: "",
498
+ usage: {}, tools: [], denials: [], artifacts: [], documents: [], changes: [], tasks: [], agents: [],
499
+ };
500
+ }
501
+ // ---- control requests -----------------------------------------------------------------------
502
+ async request(command, responseType, options = {}) {
503
+ const payload = { ...command };
504
+ if (typeof payload.request_id !== "string" || !payload.request_id)
505
+ payload.request_id = newId("req");
506
+ const wait = options.timeoutMs === undefined
507
+ ? this.init.requestTimeoutMs : Math.max(options.timeoutMs, this.init.requestTimeoutMs);
508
+ return this.transport.request(payload, responseType, { timeoutMs: wait, uncorrelatedReply: options.uncorrelatedReply });
509
+ }
510
+ /** Hold the stateDir lock around commands that make DGC save the isolated config. */
511
+ configScope(work) {
512
+ const lock = this.init.stateLock;
513
+ if (!lock || !this.init.isolated)
514
+ return work();
515
+ return lock.hold(work);
516
+ }
517
+ async listSessions() {
518
+ const event = await this.request({ type: "list_sessions", request_id: newId("sessions") }, "sessions");
519
+ return rowsToSessions(event.items);
520
+ }
521
+ async listCheckpoints() {
522
+ const event = await this.request({ type: "list_checkpoints", request_id: newId("ck") }, "checkpoints");
523
+ return (Array.isArray(event.items) ? event.items : []).filter((row) => row && typeof row === "object").map((row) => {
524
+ const item = row;
525
+ return { index: Number(item.index || 0), preview: String(item.preview || ""), files: Number(item.files || 0) };
526
+ });
527
+ }
528
+ async rewind(index) {
529
+ const event = await this.request({ type: "rewind", index, request_id: newId("rw") }, "rewound");
530
+ if (!event.ok) {
531
+ throw new DGCConfigError(`could not rewind to checkpoint ${index}; listCheckpoints() shows the valid indexes`);
532
+ }
533
+ await this.discardIdle();
534
+ return event;
535
+ }
536
+ async fork(name) {
537
+ const command = { type: "fork_session", request_id: newId("fork") };
538
+ if (name)
539
+ command.name = name;
540
+ const event = await this.request(command, "session");
541
+ const forked = String(event.session_id || this.sessionId);
542
+ if (event.path)
543
+ this.sessionPath = String(event.path);
544
+ else if (forked !== this.sessionId)
545
+ this.sessionPath = ""; // the parent's transcript is not the fork's
546
+ this.sessionId = forked;
547
+ await this.discardIdle();
548
+ if (!this.sessionPath) {
549
+ try {
550
+ await this.bindIdentity();
551
+ }
552
+ catch { /* bound after the next run */ }
553
+ }
554
+ return event;
555
+ }
556
+ async history() {
557
+ return this.request({ type: "get_history", request_id: newId("hist") }, "history");
558
+ }
559
+ async listSkills() {
560
+ const event = await this.request({ type: "list_skills", request_id: newId("skills") }, "skill_catalog");
561
+ return (Array.isArray(event.items) ? event.items : []).filter((row) => row && typeof row === "object").map((row) => {
562
+ const item = row;
563
+ return {
564
+ name: String(item.name || ""), description: String(item.description || ""),
565
+ source: String(item.source || ""), enabled: item.enabled !== false,
566
+ };
567
+ });
568
+ }
569
+ async getGoal() {
570
+ return this.request({ type: "get_goal", request_id: newId("goal") }, "goal_changed");
571
+ }
572
+ async setGoal(text, status = "active") {
573
+ return this.request({ type: "set_goal", text, status, request_id: newId("setgoal") }, "goal_changed");
574
+ }
575
+ async listMonitors() {
576
+ const event = await this.request({ type: "list_monitors", request_id: newId("mon") }, "monitors");
577
+ return Array.isArray(event.items) ? event.items : [];
578
+ }
579
+ async listHooks() {
580
+ return this.request({ type: "list_hooks", request_id: newId("hooks") }, "hook_catalog");
581
+ }
582
+ async getMemory() {
583
+ return this.request({ type: "get_memory", request_id: newId("mem") }, "memory");
584
+ }
585
+ async addMemory(text, scope = "project") {
586
+ if (scope !== "project" && scope !== "user")
587
+ throw new DGCConfigError("memory scope must be 'project' or 'user'");
588
+ return this.request({ type: "add_memory", text, scope, request_id: newId("addmem") }, "memory");
589
+ }
590
+ async listPermissions() {
591
+ const event = await this.request({ type: "list_permissions", request_id: newId("perms") }, "permissions");
592
+ return permissionRows(event.items);
593
+ }
594
+ /**
595
+ * Add a rule. In an isolated session it lasts for this state_dir's sessions until the next
596
+ * session rewrites the config; a RuntimePolicy is not installed this way (it is per session).
597
+ */
598
+ async addPermissionRule(action, rule) {
599
+ if (!["allow", "ask", "deny"].includes(action))
600
+ throw new DGCConfigError("permission action must be allow, ask, or deny");
601
+ const event = await this.configScope(() => this.request({ type: "add_permission_rule", action, rule, request_id: newId("addperm") }, "permissions"));
602
+ return permissionRows(event.items);
603
+ }
604
+ async removePermissionRule(action, rule) {
605
+ const event = await this.configScope(() => this.request({ type: "remove_permission_rule", action, rule, request_id: newId("rmperm") }, "permissions"));
606
+ return permissionRows(event.items);
607
+ }
608
+ async addMcpServer(name, command, args = []) {
609
+ const runtime = { transport: "stdio", command, args: [...args], env: {}, env_names: [], log_level: "warning" };
610
+ const { env: _env, ...persisted } = runtime;
611
+ const event = await this.configScope(() => this.request({
612
+ type: "upsert_mcp_server", request_id: newId("mcpadd"), name, runtime, persisted,
613
+ }, "mcp_servers", { timeoutMs: 20_000 }));
614
+ return Array.isArray(event.items) ? event.items : [];
615
+ }
616
+ async listMcpServers() {
617
+ const event = await this.request({ type: "list_mcp_servers", request_id: newId("mcp") }, "mcp_servers");
618
+ return Array.isArray(event.items) ? event.items : [];
619
+ }
620
+ async getSkill(name) {
621
+ return this.request({ type: "get_skill", name, request_id: newId("skill") }, "skill_detail");
622
+ }
623
+ async setSkillEnabled(name, enabled) {
624
+ return this.request({ type: "set_skill_enabled", name, enabled, request_id: newId("sken") }, "skill_catalog");
625
+ }
626
+ async stopMonitor(id = "all") {
627
+ const event = await this.request({ type: "stop_monitor", id, request_id: newId("stopmon") }, "monitors");
628
+ return Array.isArray(event.items) ? event.items : [];
629
+ }
630
+ async listArtifacts() {
631
+ const event = await this.request({ type: "list_artifacts", request_id: newId("arts") }, "artifacts");
632
+ return (Array.isArray(event.items) ? event.items : []).filter((row) => row && typeof row === "object").map((row) => {
633
+ const item = row;
634
+ return { id: String(item.id || ""), name: String(item.name || ""), url: String(item.url || ""), rel: String(item.rel || item.path || "") };
635
+ });
636
+ }
637
+ async listAgents() {
638
+ const event = await this.request({ type: "list_agents", request_id: newId("agents") }, "agents");
639
+ return Array.isArray(event.items) ? event.items : [];
640
+ }
641
+ async clearTodos() {
642
+ // DGC acknowledges with the uncorrelated `todos` event every frontend hears.
643
+ const event = await this.request({ type: "clear_todos", request_id: newId("cleartodo") }, "todos", { uncorrelatedReply: true });
644
+ this.taskIds.clear();
645
+ this.taskRevision += 1;
646
+ return this.projectTasks(event.todos);
647
+ }
648
+ async getPlan() {
649
+ return this.request({ type: "get_plan", request_id: newId("plan") }, "saved_plan");
650
+ }
651
+ async getConfig() {
652
+ return this.request({ type: "get_config", request_id: newId("cfgget") }, "config");
653
+ }
654
+ async getUsage(range = "today") {
655
+ return this.request({ type: "get_usage", range, request_id: newId("usage") }, "usage_report");
656
+ }
657
+ async newSession() {
658
+ const event = await this.request({ type: "new_session", request_id: newId("new") }, "session");
659
+ this.sessionId = String(event.session_id || this.sessionId);
660
+ this.sessionPath = String(event.path || "");
661
+ await this.discardIdle();
662
+ return event;
663
+ }
664
+ async nameSession(name) {
665
+ return this.request({ type: "name_session", name, request_id: newId("name") }, "session_named");
666
+ }
667
+ async deleteSession(path) {
668
+ const event = await this.request({ type: "delete_session", path, request_id: newId("del") }, "sessions");
669
+ return rowsToSessions(event.items);
670
+ }
671
+ async generateHandoff(save = false) {
672
+ return this.request({ type: "generate_handoff", save, request_id: newId("handoff") }, "handoff", { timeoutMs: 60_000 });
673
+ }
674
+ // ---- reading the pipe -----------------------------------------------------------------------
675
+ async read(timeoutMs) {
676
+ const pending = this.pending.shift();
677
+ if (pending)
678
+ return pending;
679
+ const event = await this.transport.next(Math.max(10, timeoutMs));
680
+ const [owner, scoped] = this.observe(event);
681
+ return { event, owner, scoped };
682
+ }
683
+ /**
684
+ * Bookkeeping every event gets, whichever reader takes it: turn ownership, prompt outcomes,
685
+ * usage billing, the audit row, and identity.
686
+ */
687
+ observe(event) {
688
+ const kind = String(event.type || "");
689
+ const rid = typeof event.request_id === "string" ? event.request_id : "";
690
+ let owner = null;
691
+ let scoped = false;
692
+ if (kind === "turn_start") {
693
+ const tid = String(event.turn_id || "");
694
+ owner = rid ? this.owners.get(rid) || null : null;
695
+ if (!owner && !rid && String(event.kind || "prompt") === "prompt")
696
+ owner = this.adoptable();
697
+ if (owner) {
698
+ const started = rid || owner.requestId;
699
+ owner.turnIds.add(tid);
700
+ owner.startedIds.add(started);
701
+ owner.expect.delete(started);
702
+ owner.liveTurn = tid;
703
+ owner.started = true;
704
+ }
705
+ this.turn = { id: tid, owner, requestId: rid };
706
+ scoped = true;
707
+ }
708
+ else if (kind === "turn_end") {
709
+ const tid = String(event.turn_id || "");
710
+ const turn = this.turn;
711
+ if (turn && turn.id === tid) {
712
+ owner = turn.owner;
713
+ this.turn = null;
714
+ }
715
+ else {
716
+ owner = [...this.owners.values()].find((run) => run.turnIds.has(tid)) || null;
717
+ }
718
+ if (owner) {
719
+ owner.liveTurn = "";
720
+ owner.pendingBills += 1;
721
+ }
722
+ this.billTo = [owner, tid];
723
+ scoped = true;
724
+ }
725
+ else if (this.turn && !rid) {
726
+ owner = this.turn.owner;
727
+ scoped = true;
728
+ }
729
+ if (kind === "context")
730
+ this.account(event);
731
+ else if (["prompt_accepted", "steering_update", "command_rejected", "error"].includes(kind) && rid) {
732
+ this.noteOutcome(kind, rid, event);
733
+ }
734
+ else if ((kind === "model_changed" || kind === "config") && event.model) {
735
+ this.modelSeen = String(event.model);
736
+ }
737
+ else if (kind === "session" && !rid) {
738
+ this.sessionId = String(event.session_id || this.sessionId);
739
+ if (event.path)
740
+ this.sessionPath = String(event.path);
741
+ }
742
+ let runId;
743
+ if (owner)
744
+ runId = owner.result.runId;
745
+ else if (rid && this.owners.has(rid))
746
+ runId = this.owners.get(rid).result.runId;
747
+ else if (scoped)
748
+ runId = `turn-${String(event.turn_id || this.turn?.id || "")}`;
749
+ else
750
+ runId = this.active ? this.active.result.runId : "";
751
+ if (this.init.auditLog && AUDIT_EVENTS.has(kind)) {
752
+ try {
753
+ const payload = {};
754
+ for (const key of AUDIT_FIELDS) {
755
+ const value = event[key];
756
+ if (value === undefined || value === null || value === "")
757
+ continue;
758
+ if (Array.isArray(value) && !value.length)
759
+ continue;
760
+ if (typeof value === "object" && !Array.isArray(value) && !Object.keys(value).length)
761
+ continue;
762
+ payload[key] = value;
763
+ }
764
+ this.init.auditLog.append(this.sessionId, runId, kind, payload, !(this.init.policy && !this.init.policy.redactEvents));
765
+ }
766
+ catch (error) {
767
+ warn(`dgc sdk: could not write an audit row: ${String(error)}`);
768
+ }
769
+ }
770
+ return [owner, scoped];
771
+ }
772
+ /** A workflow prompt's turn carries no request id; give it to the run waiting for it. */
773
+ adoptable() {
774
+ const run = this.active;
775
+ return run && run.expect.has(run.requestId) ? run : null;
776
+ }
777
+ noteOutcome(kind, rid, event) {
778
+ const run = this.owners.get(rid);
779
+ if (!run)
780
+ return;
781
+ const state = String(event.state || "");
782
+ if (kind === "prompt_accepted") {
783
+ run.unresolved.delete(rid);
784
+ if ((state === "started" || state === "queued") && run.steers.has(rid) && !run.startedIds.has(rid)) {
785
+ run.expect.add(rid); // the steer became a turn of its own
786
+ }
787
+ }
788
+ else if (kind === "steering_update") {
789
+ run.unresolved.delete(rid);
790
+ if (state === "queued" && !run.startedIds.has(rid))
791
+ run.expect.add(rid);
792
+ else if (state === "returned") {
793
+ run.expect.delete(rid);
794
+ run.returned.add(rid);
795
+ }
796
+ }
797
+ else {
798
+ run.unresolved.delete(rid);
799
+ run.expect.delete(rid);
800
+ run.rejected.set(rid, String(event.message || event.reason || kind));
801
+ }
802
+ }
803
+ account(event) {
804
+ const totals = usageTotals(event);
805
+ if (!totals)
806
+ return;
807
+ const bill = this.billTo;
808
+ this.billTo = null;
809
+ if (bill) {
810
+ const [owner, tid] = bill;
811
+ const delta = this.usageLast ? usageDelta(this.usageLast, totals) : null;
812
+ if (owner) {
813
+ owner.pendingBills = Math.max(0, owner.pendingBills - 1);
814
+ owner.billed += 1;
815
+ if (!delta || !reported(delta)) {
816
+ owner.usageUnknown = true;
817
+ // DGC still counted the requests; only their token usage is unknown.
818
+ if (delta)
819
+ owner.usage.requests += delta.requests;
820
+ }
821
+ else {
822
+ for (const key of USAGE_TOTAL_KEYS)
823
+ owner.usage[key] += delta[key];
824
+ }
825
+ }
826
+ else if (delta && delta.requests && this.init.usageLog) {
827
+ // A turn no run owns (a goal resume, a late steer): its spend is still recorded.
828
+ const known = reported(delta);
829
+ const row = { run_id: `turn-${tid}`, status: "unowned" };
830
+ for (const key of USAGE_TOTAL_KEYS)
831
+ row[key] = known || key === "requests" ? delta[key] : null;
832
+ this.recordUsage(row);
833
+ }
834
+ }
835
+ this.usageLast = totals;
836
+ }
837
+ /** Drop control events left on the pipe (after resume/fork/rewind); stop at anything else. */
838
+ async discardIdle(timeoutMs = 250) {
839
+ const deadline = Date.now() + Math.max(50, timeoutMs);
840
+ while (Date.now() < deadline) {
841
+ let item;
842
+ try {
843
+ item = await this.read(Math.min(50, Math.max(20, deadline - Date.now())));
844
+ }
845
+ catch {
846
+ return;
847
+ }
848
+ if (IDLE_EVENT_TYPES.has(String(item.event.type || "")))
849
+ continue;
850
+ this.pending.unshift(item);
851
+ return;
852
+ }
853
+ }
854
+ // ---- cancellation ---------------------------------------------------------------------------
855
+ /** @internal */
856
+ cancelRun(run, reason) {
857
+ if (run.done)
858
+ return;
859
+ if (!run.cancelReason) {
860
+ run.cancelReason = reason;
861
+ run.noteCancel();
862
+ }
863
+ // A follow-up still queued behind another run is cancelled when its turn starts; DGC's cancel
864
+ // would stop the run in front of it too.
865
+ const send = run === this.active && !run.cancelSent;
866
+ if (send) {
867
+ run.cancelSent = true;
868
+ run.cancelAt = Date.now();
869
+ try {
870
+ this.transport.send({ type: "cancel" });
871
+ }
872
+ catch (error) {
873
+ throw new DGCRuntimeError("could not cancel the run", { cause: error });
874
+ }
875
+ }
876
+ }
877
+ /** @internal Drop a queued run nobody will drive. */
878
+ forget(run) {
879
+ this.queued = this.queued.filter((item) => item !== run);
880
+ for (const rid of run.ids)
881
+ if (this.owners.get(rid) === run)
882
+ this.owners.delete(rid);
883
+ if (this.active === run)
884
+ this.active = null;
885
+ if (!TERMINAL.has(run.result.status)) {
886
+ run.result.status = "cancelled";
887
+ run.result.reason = "cancelled";
888
+ }
889
+ run.markDone();
890
+ }
891
+ // ---- the pump -------------------------------------------------------------------------------
892
+ composePrompt(prompt) {
893
+ const instructions = this.init.instructions.trim();
894
+ if (!instructions)
895
+ return prompt;
896
+ return `<application-instructions>\n${instructions}\n</application-instructions>\n\n${prompt}`;
897
+ }
898
+ /** Apply a run-scoped max_turns; DGC persists set_config, so it is restored after. */
899
+ async setRunMaxTurns(value) {
900
+ const deadline = Date.now() + 5_000;
901
+ for (;;) {
902
+ try {
903
+ await this.configScope(() => this.request({ type: "set_config", values: { max_turns: value }, request_id: newId("cfg") }, "config"));
904
+ return;
905
+ }
906
+ catch (error) {
907
+ if (error instanceof DGCCommandRejectedError && error.reason === "turn_in_progress" && Date.now() < deadline) {
908
+ await sleep(100);
909
+ continue;
910
+ }
911
+ throw error;
912
+ }
913
+ }
914
+ }
915
+ async restoreMaxTurns() {
916
+ try {
917
+ await this.setRunMaxTurns(this.init.maxTurns ?? 0);
918
+ this.maxTurnsDirty = false;
919
+ }
920
+ catch (error) {
921
+ warn(`dgc sdk: could not restore the session maxTurns: ${String(error)}`);
922
+ }
923
+ }
924
+ sendPrompt(run, text, skills, workflow, requestId) {
925
+ const payload = { type: "prompt", text, request_id: requestId };
926
+ if (skills?.length)
927
+ payload.skills = [...skills];
928
+ if (workflow)
929
+ payload.workflow = workflow;
930
+ run.ids.add(requestId);
931
+ run.expect.add(requestId);
932
+ this.owners.set(requestId, run);
933
+ this.transport.send(payload);
934
+ }
935
+ async *pump(run, prompt, timeoutMs, maxTurns, outputSchema, skills, workflow, repairAttempts, send, prior) {
936
+ const result = run.result;
937
+ const acc = new Accumulator(result);
938
+ let normal = false;
939
+ let scoped = false;
940
+ let before = new Map();
941
+ try {
942
+ run.pumpStarted = true;
943
+ if (prior) {
944
+ if (prior.handle) {
945
+ try {
946
+ await prior.handle.result();
947
+ }
948
+ catch { /* its own caller sees that */ }
949
+ }
950
+ await prior.waitDone(CANCEL_GRACE_MS + 5_000);
951
+ this.queued = this.queued.filter((item) => item !== run);
952
+ if (!this.active || this.active.done)
953
+ this.active = run;
954
+ result.status = "running";
955
+ }
956
+ // DGC starts a queued follow-up as soon as the turn in front of it ends, so its "before" is
957
+ // the workspace at that moment, not whenever this pump got here.
958
+ before = (!send && prior?.after ? prior.after : null) ?? snapshotWorkspace(this.init.cwd, this.init.excludePaths);
959
+ const stopped = run.held && (Boolean(prior?.cancelReason) || prior?.result.status === "cancelled");
960
+ if ((send && run.cancelReason) || (run.held && (run.cancelReason || stopped))) {
961
+ // Cancelled before its prompt went out (an aborted signal), or a held follow-up behind a
962
+ // stopped run: DGC hands queued prompts back when the run in front is stopped; so does this.
963
+ acc.cancel();
964
+ if (run.held && run.cancelReason !== "cancelled")
965
+ acc.error = "DGC stopped before this queued prompt ran";
966
+ await this.finish(run, acc, before);
967
+ normal = true;
968
+ return;
969
+ }
970
+ if (send) {
971
+ if (maxTurns !== undefined) {
972
+ this.maxTurnsDirty = true;
973
+ await this.setRunMaxTurns(maxTurns);
974
+ scoped = true;
975
+ }
976
+ else if (this.maxTurnsDirty) {
977
+ await this.restoreMaxTurns(); // an abandoned run left its own max_turns behind
978
+ }
979
+ this.sendPrompt(run, this.composePrompt(prompt), skills, workflow, run.requestId);
980
+ }
981
+ yield* this.turns(run, acc, timeoutMs, false);
982
+ if (outputSchema && acc.status === "completed") {
983
+ this.applySchema(result, outputSchema, acc);
984
+ let attempts = Math.max(0, Math.trunc(repairAttempts));
985
+ while (result.output === undefined && attempts > 0 && acc.status === "completed" && !run.cancelReason) {
986
+ attempts -= 1;
987
+ const repair = "Return only valid JSON matching this schema. Do not call tools. Do not edit files.\n"
988
+ + JSON.stringify(outputSchema)
989
+ + `\nPrevious errors: ${acc.error || "the last answer was not valid JSON"}`;
990
+ acc.error = undefined;
991
+ if (this.init.isolated) {
992
+ try {
993
+ this.maxTurnsDirty = true;
994
+ await this.setRunMaxTurns(1);
995
+ scoped = true;
996
+ }
997
+ catch (error) {
998
+ warn(`dgc sdk: repair runs without a turn cap: ${String(error)}`);
999
+ }
1000
+ }
1001
+ this.sendPrompt(run, repair, undefined, undefined, newId("repair"));
1002
+ yield* this.turns(run, acc, timeoutMs, true);
1003
+ if (acc.status === "completed")
1004
+ this.applySchema(result, outputSchema, acc);
1005
+ }
1006
+ }
1007
+ if (scoped) {
1008
+ scoped = false;
1009
+ await this.restoreMaxTurns();
1010
+ }
1011
+ await this.finish(run, acc, before);
1012
+ normal = true;
1013
+ }
1014
+ catch (error) {
1015
+ if (!(error instanceof DGCError) || !this.transport.closed)
1016
+ throw error;
1017
+ // The backend went away under this run.
1018
+ normal = true;
1019
+ const cancelled = run.cancelReason === "cancelled";
1020
+ result.status = cancelled ? "cancelled" : "failed";
1021
+ result.reason = cancelled ? "cancelled" : "transport";
1022
+ result.error = cancelled ? undefined : error.message;
1023
+ }
1024
+ finally {
1025
+ if (!normal && !run.done && !TERMINAL.has(result.status)) {
1026
+ // The consumer left early (break / return): stop the agent too.
1027
+ try {
1028
+ this.cancelRun(run, "cancelled");
1029
+ }
1030
+ catch { /* the transport is gone */ }
1031
+ result.status = "cancelled";
1032
+ result.reason = "cancelled";
1033
+ }
1034
+ if (scoped && normal)
1035
+ await this.restoreMaxTurns();
1036
+ if (!TERMINAL.has(result.status)) {
1037
+ result.status = "failed";
1038
+ result.reason = result.reason || "error";
1039
+ }
1040
+ if (this.active === run)
1041
+ this.active = null;
1042
+ this.queued = this.queued.filter((item) => item !== run);
1043
+ for (const rid of run.ids)
1044
+ if (this.owners.get(rid) === run)
1045
+ this.owners.delete(rid);
1046
+ run.markDone();
1047
+ }
1048
+ }
1049
+ /** Pump events until every turn this run owns has ended. */
1050
+ async *turns(run, acc, timeoutMs, repairing) {
1051
+ const result = run.result;
1052
+ const deadline = timeoutMs === null ? null : Date.now() + timeoutMs;
1053
+ let grace = null; // after our last turn: waiting for a steer's outcome
1054
+ let endedAny = false;
1055
+ for (;;) {
1056
+ const now = Date.now();
1057
+ const rejected = endedAny ? undefined : run.rejected.get(run.requestId);
1058
+ const returned = run.returned.has(run.requestId) && !run.started;
1059
+ if (rejected !== undefined) {
1060
+ if (run.cancelReason === "cancelled")
1061
+ acc.cancel();
1062
+ else
1063
+ acc.fail("rejected", rejected);
1064
+ return;
1065
+ }
1066
+ if (returned) {
1067
+ acc.cancel();
1068
+ acc.error = "DGC stopped before this queued prompt ran";
1069
+ return;
1070
+ }
1071
+ if (run.cancelReason && !run.cancelSent) {
1072
+ try {
1073
+ this.cancelRun(run, run.cancelReason);
1074
+ }
1075
+ catch { /* reported by the next read */ }
1076
+ }
1077
+ if (deadline !== null && now >= deadline && !run.cancelReason) {
1078
+ acc.timeoutMs = timeoutMs || 0;
1079
+ acc.partial();
1080
+ try {
1081
+ this.cancelRun(run, "timeout");
1082
+ }
1083
+ catch { /* reported by the next read */ }
1084
+ }
1085
+ if (run.cancelSent) {
1086
+ const limit = run.cancelReason === "timeout" ? TIMEOUT_GRACE_MS : CANCEL_GRACE_MS;
1087
+ if (now - run.cancelAt >= limit) {
1088
+ acc.stopped(run, timeoutMs);
1089
+ return;
1090
+ }
1091
+ }
1092
+ if (grace !== null) {
1093
+ const state = this.completion(run);
1094
+ if (state === "done" || (state === "steer" && now >= grace))
1095
+ return;
1096
+ if (state === "turn")
1097
+ grace = null;
1098
+ }
1099
+ let wait = SLICE_MS;
1100
+ if (deadline !== null && !run.cancelReason)
1101
+ wait = Math.max(10, Math.min(wait, deadline - now));
1102
+ let item;
1103
+ try {
1104
+ item = await this.read(wait);
1105
+ }
1106
+ catch (error) {
1107
+ if (error instanceof DGCTimeoutError)
1108
+ continue;
1109
+ acc.transport(run, error);
1110
+ return;
1111
+ }
1112
+ const { event, owner, scoped } = item;
1113
+ const kind = String(event.type || "");
1114
+ if (kind === "ready")
1115
+ continue;
1116
+ const mine = scoped && owner === run;
1117
+ if (mine && kind === "turn_end" && !run.expect.size && !run.unresolved.size) {
1118
+ // This run's last turn just ended; a queued follow-up may start any moment now.
1119
+ run.after = snapshotWorkspace(this.init.cwd, this.init.excludePaths);
1120
+ }
1121
+ if (scoped && !mine && !DECISION_EVENTS.has(kind))
1122
+ continue; // observed and audited, not reported
1123
+ if (!scoped || mine) {
1124
+ yield {
1125
+ type: kind, data: event, sessionId: this.sessionId, runId: result.runId,
1126
+ requestId: typeof event.request_id === "string" ? event.request_id : run.requestId,
1127
+ };
1128
+ }
1129
+ if (DECISION_EVENTS.has(kind)) {
1130
+ // A decision blocks DGC whoever's turn it is, so it is always answered.
1131
+ await this.answer(run, event, kind, repairing, mine || !scoped);
1132
+ continue;
1133
+ }
1134
+ if (!mine) {
1135
+ if (["context", "artifact_ready", "agent_started", "agent_ended"].includes(kind))
1136
+ acc.take(kind, event, this);
1137
+ continue;
1138
+ }
1139
+ if (kind === "turn_end") {
1140
+ endedAny = true;
1141
+ acc.endTurn(event);
1142
+ if (run.cancelReason)
1143
+ return;
1144
+ const state = this.completion(run);
1145
+ if (state === "done")
1146
+ return;
1147
+ if (state === "steer")
1148
+ grace = Date.now() + STEER_GRACE_MS;
1149
+ continue;
1150
+ }
1151
+ acc.take(kind, event, this);
1152
+ if (kind === "error" && event.fatal) {
1153
+ acc.fatalError(run, event);
1154
+ return;
1155
+ }
1156
+ }
1157
+ }
1158
+ completion(run) {
1159
+ if (run.expect.size)
1160
+ return "turn";
1161
+ if (run.unresolved.size)
1162
+ return "steer";
1163
+ return "done";
1164
+ }
1165
+ /** The usage totals for a turn arrive right after its turn_end. */
1166
+ async awaitBilling(run) {
1167
+ const deadline = Date.now() + BILLING_WAIT_MS;
1168
+ const held = [];
1169
+ try {
1170
+ while (run.pendingBills > 0 && Date.now() < deadline) {
1171
+ let item;
1172
+ try {
1173
+ item = await this.read(Math.max(10, Math.min(200, deadline - Date.now())));
1174
+ }
1175
+ catch (error) {
1176
+ if (error instanceof DGCTimeoutError)
1177
+ continue;
1178
+ return;
1179
+ }
1180
+ if (item.event.type !== "context")
1181
+ held.push(item);
1182
+ }
1183
+ }
1184
+ finally {
1185
+ this.pending.unshift(...held);
1186
+ }
1187
+ }
1188
+ async finish(run, acc, before) {
1189
+ const result = run.result;
1190
+ if (run.pendingBills)
1191
+ await this.awaitBilling(run);
1192
+ const status = acc.settle(run);
1193
+ result.changes = diffWorkspace(this.init.cwd, before, this.init.excludePaths, run.after);
1194
+ result.verification = acc.verification(this.init.verifyCommand);
1195
+ this.finishUsage(run, acc, status);
1196
+ if (!this.sessionPath && !this.transport.closed) {
1197
+ try {
1198
+ await this.bindIdentity();
1199
+ }
1200
+ catch { /* bound after a later run */ }
1201
+ }
1202
+ result.status = status; // last: a terminal status means the result is complete
1203
+ }
1204
+ applySchema(result, schema, acc) {
1205
+ let parsed;
1206
+ try {
1207
+ parsed = extractJson(result.finalText);
1208
+ }
1209
+ catch (error) {
1210
+ acc.error = `output_schema: ${error instanceof Error ? error.message : String(error)}`;
1211
+ result.output = undefined;
1212
+ return;
1213
+ }
1214
+ const problems = validateSchema(parsed, schema);
1215
+ if (problems.length) {
1216
+ acc.error = "output_schema: " + problems.slice(0, 8).join("; ");
1217
+ result.output = undefined;
1218
+ return;
1219
+ }
1220
+ result.output = parsed;
1221
+ acc.error = undefined;
1222
+ }
1223
+ /** @internal */
1224
+ projectTasks(rows) {
1225
+ this.taskRevision += 1;
1226
+ const items = [];
1227
+ const seen = new Map();
1228
+ if (!Array.isArray(rows))
1229
+ return items;
1230
+ for (const row of rows) {
1231
+ if (!row || typeof row !== "object")
1232
+ continue;
1233
+ const item = row;
1234
+ const content = String(item.content || item.text || "");
1235
+ const key = content.trim().toLowerCase();
1236
+ const tid = String(item.id || this.taskIds.get(key) || `task-${randomUUID().replace(/-/g, "").slice(0, 10)}`);
1237
+ if (key) {
1238
+ seen.set(key, tid);
1239
+ this.taskIds.set(key, tid);
1240
+ }
1241
+ const status = TASK_MAP[String(item.status || "").trim().toLowerCase()] || "pending";
1242
+ items.push({ id: tid, content, status, revision: this.taskRevision });
1243
+ }
1244
+ for (const key of [...this.taskIds.keys()])
1245
+ if (!seen.has(key))
1246
+ this.taskIds.delete(key);
1247
+ return items;
1248
+ }
1249
+ recordUsage(row) {
1250
+ if (!this.init.usageLog)
1251
+ return;
1252
+ try {
1253
+ this.init.usageLog.record({
1254
+ session_id: this.sessionId, department: this.init.department,
1255
+ model: this.modelSeen || this.init.model, ...row,
1256
+ });
1257
+ }
1258
+ catch (error) {
1259
+ warn(`dgc sdk: could not record usage: ${String(error)}`);
1260
+ }
1261
+ }
1262
+ finishUsage(run, acc, status) {
1263
+ const result = run.result;
1264
+ const usage = { ...acc.usage };
1265
+ const known = run.billed > 0 && !run.usageUnknown;
1266
+ for (const key of USAGE_TOTAL_KEYS) {
1267
+ if (key === "requests")
1268
+ usage[key] = run.billed ? run.usage[key] : null;
1269
+ else
1270
+ usage[key] = known ? run.usage[key] : null;
1271
+ }
1272
+ const dollars = known ? costUsd(usage.input_tokens, usage.output_tokens, usage.cached_input_tokens, this.init.pricing) : null;
1273
+ usage.cost_usd = dollars;
1274
+ usage.usage_known = known;
1275
+ usage.department = this.init.department;
1276
+ usage.model = this.modelSeen || this.init.model;
1277
+ result.usage = usage;
1278
+ const row = {
1279
+ session_id: result.sessionId || this.sessionId, run_id: result.runId, status,
1280
+ };
1281
+ for (const key of USAGE_TOTAL_KEYS)
1282
+ row[key] = usage[key];
1283
+ row.token_estimate = usage.token_estimate ?? null;
1284
+ row.cost_usd = dollars;
1285
+ this.recordUsage(row);
1286
+ }
1287
+ // ---- decisions ------------------------------------------------------------------------------
1288
+ async answer(run, event, kind, repairing, mine) {
1289
+ if (mine)
1290
+ run.result.status = "waiting_for_approval";
1291
+ try {
1292
+ if (kind === "permission_request") {
1293
+ const { action, reason } = run.cancelReason || repairing
1294
+ ? { action: "deny", reason: "" } : await this.permission(run, event, mine);
1295
+ const command = { type: "permission_response", id: event.id, decision: action };
1296
+ // So the runtime does not report the application's policy denial as "Denied by the user".
1297
+ if (action === "deny" && reason)
1298
+ command.reason = reason;
1299
+ this.respond(command, run);
1300
+ }
1301
+ else if (kind === "plan_proposal") {
1302
+ const decision = repairing || run.cancelReason ? "reject" : await this.plan(run, event, mine);
1303
+ this.respond({ type: "plan_response", id: event.id, decision }, run);
1304
+ }
1305
+ else if (kind === "options_request") {
1306
+ await this.answerOptions(run, event, mine);
1307
+ }
1308
+ else if (kind === "mcp_input_request") {
1309
+ await this.answerMcp(run, event, mine);
1310
+ }
1311
+ }
1312
+ finally {
1313
+ if (mine && run.result.status === "waiting_for_approval")
1314
+ run.result.status = "running";
1315
+ }
1316
+ if (mine && run.decisionError && this.init.unhandled === "callback" && !run.cancelReason) {
1317
+ try {
1318
+ this.cancelRun(run, "decision_failed");
1319
+ }
1320
+ catch { /* reported by the next read */ }
1321
+ }
1322
+ }
1323
+ respond(command, run) {
1324
+ try {
1325
+ this.transport.send(command);
1326
+ }
1327
+ catch (error) {
1328
+ if (!run.cancelReason)
1329
+ throw error;
1330
+ }
1331
+ }
1332
+ /**
1333
+ * Ask an application callback (sync or async). `decisionTimeoutMs: null` waits as long as it
1334
+ * takes; a cancel always wins. A callback that throws, times out, or returns an invalid answer
1335
+ * gets `fallback`; with `permissions.unhandled: "callback"` that also stops the run.
1336
+ */
1337
+ async decide(run, callback, request, fallback, label, valid, mine) {
1338
+ const key = request.id || "";
1339
+ if (key) {
1340
+ if (this.answeredIds.has(key))
1341
+ return fallback;
1342
+ this.answeredIds.add(key);
1343
+ }
1344
+ if (!callback)
1345
+ return fallback;
1346
+ const limit = this.init.options.decisionTimeoutMs === undefined ? 30_000 : this.init.options.decisionTimeoutMs;
1347
+ let stop = () => { };
1348
+ const outcome = await Promise.race([
1349
+ Promise.resolve().then(() => callback(request))
1350
+ .then((value) => ({ kind: "value", value }), (error) => ({ kind: "error", error })),
1351
+ run.cancelled().then(() => ({ kind: "cancel" })),
1352
+ new Promise((resolve) => {
1353
+ stop = longTimer(limit === null ? null : Math.max(50, limit), () => resolve({ kind: "timeout" }));
1354
+ }),
1355
+ ]);
1356
+ stop();
1357
+ if (outcome.kind === "cancel")
1358
+ return fallback;
1359
+ let problem = "";
1360
+ if (outcome.kind === "timeout")
1361
+ problem = `${label} did not answer within ${limit} ms`;
1362
+ else if (outcome.kind === "error") {
1363
+ const error = outcome.error;
1364
+ problem = `${label} threw ${error instanceof Error ? `${error.name}: ${error.message}` : String(error)}`;
1365
+ }
1366
+ else if (!valid(outcome.value)) {
1367
+ let shown;
1368
+ try {
1369
+ shown = JSON.stringify(outcome.value);
1370
+ }
1371
+ catch {
1372
+ shown = String(outcome.value);
1373
+ }
1374
+ problem = `${label} returned an invalid answer: ${shown}`;
1375
+ }
1376
+ if (problem) {
1377
+ warn(`dgc sdk: ${problem}; answering ${JSON.stringify(fallback)}`);
1378
+ if (mine && !run.decisionError)
1379
+ run.decisionError = problem;
1380
+ return fallback;
1381
+ }
1382
+ return outcome.value;
1383
+ }
1384
+ async permission(run, event, mine) {
1385
+ const request = {
1386
+ id: String(event.id || ""),
1387
+ name: String(event.name || ""),
1388
+ args: mapping(event.args),
1389
+ summary: event.summary ? String(event.summary) : undefined,
1390
+ suggestedRule: event.suggested_rule ? String(event.suggested_rule) : undefined,
1391
+ callId: typeof event.call_id === "string" ? event.call_id : undefined,
1392
+ diff: typeof event.diff === "string" ? event.diff : undefined,
1393
+ command: typeof event.command === "string" ? event.command : undefined,
1394
+ };
1395
+ // Policy denies (tools, paths) are final; command screening defers to a reviewing callback;
1396
+ // policy-checked reads are answered here.
1397
+ return resolvePermission(this.init.policy, request, {
1398
+ cwd: this.init.cwd, permissionMode: this.init.permissionMode, onPermission: this.init.options.onPermission,
1399
+ ask: () => this.decide(run, this.init.options.onPermission, request, "deny", "onPermission", (value) => value === "once" || value === "always" || value === "deny", mine),
1400
+ });
1401
+ }
1402
+ async plan(run, event, mine) {
1403
+ const request = {
1404
+ id: String(event.id || ""),
1405
+ plan: String(event.plan || ""),
1406
+ choices: Array.isArray(event.choices) ? event.choices.filter((item) => typeof item === "string") : [],
1407
+ };
1408
+ const choices = ["auto", "acceptEdits", "default", "reject"];
1409
+ const decision = await this.decide(run, this.init.options.onPlan, request, "reject", "onPlan", (value) => typeof value === "string" && choices.includes(value), mine);
1410
+ return choices.includes(decision) ? decision : "reject";
1411
+ }
1412
+ async answerOptions(run, event, mine) {
1413
+ const request = {
1414
+ id: String(event.id || ""),
1415
+ questions: Array.isArray(event.questions) ? event.questions : [],
1416
+ callId: typeof event.call_id === "string" ? event.call_id : undefined,
1417
+ };
1418
+ let answer = "dismiss";
1419
+ if (this.init.options.onQuestion && !run.cancelReason) {
1420
+ answer = await this.decide(run, this.init.options.onQuestion, request, "dismiss", "onQuestion", (value) => value === "dismiss" || (Boolean(value) && typeof value === "object" && !Array.isArray(value)), mine);
1421
+ }
1422
+ const payload = {};
1423
+ if (answer && typeof answer === "object") {
1424
+ for (const [qid, value] of Object.entries(answer)) {
1425
+ if (value && typeof value === "object")
1426
+ payload[qid] = value;
1427
+ }
1428
+ }
1429
+ if (!Object.keys(payload).length) {
1430
+ this.respond({ type: "options_response", id: request.id, dismissed: true }, run);
1431
+ return;
1432
+ }
1433
+ this.respond({ type: "options_response", id: request.id, answers: payload }, run);
1434
+ }
1435
+ async answerMcp(run, event, mine) {
1436
+ const request = {
1437
+ id: String(event.id || ""), server: String(event.server || ""), kind: String(event.kind || ""),
1438
+ payload: mapping(event.payload),
1439
+ };
1440
+ let response = null;
1441
+ if (this.init.options.onMcpInput && !run.cancelReason) {
1442
+ response = await this.decide(run, this.init.options.onMcpInput, request, null, "onMcpInput", (value) => Boolean(value) && typeof value === "object"
1443
+ && ["accept", "decline", "cancel"].includes(String(value.action)), mine);
1444
+ }
1445
+ const payload = { type: "mcp_input_response", id: request.id, action: response ? response.action : "cancel" };
1446
+ if (response?.content)
1447
+ payload.content = { ...response.content };
1448
+ this.respond(payload, run);
1449
+ }
1450
+ }
1451
+ /** What one run collects from the turns it owns. */
1452
+ class Accumulator {
1453
+ result;
1454
+ tools = new Map();
1455
+ denials = [];
1456
+ text = [];
1457
+ blocks = new Map();
1458
+ answerIds = [];
1459
+ artifacts = new Map();
1460
+ documents = new Map();
1461
+ tasks;
1462
+ agents = new Map();
1463
+ usage = {};
1464
+ status = "running";
1465
+ reason = "";
1466
+ error;
1467
+ timeoutMs = 0;
1468
+ lastError = "";
1469
+ verifyCalls = new Set();
1470
+ verifyModel = null;
1471
+ verifyCli = "";
1472
+ verifyCliMessage = "";
1473
+ verifyOrder = 0;
1474
+ verifyModelOrder = -1;
1475
+ verifyCliOrder = -1;
1476
+ constructor(result) {
1477
+ this.result = result;
1478
+ for (const item of result.artifacts)
1479
+ this.artifacts.set(item.id, item);
1480
+ for (const item of result.documents)
1481
+ this.documents.set(item.id, item);
1482
+ this.tasks = [...(result.tasks || [])];
1483
+ for (const item of result.agents || [])
1484
+ this.agents.set(item.id, item);
1485
+ }
1486
+ take(kind, event, session) {
1487
+ if (kind === "text_delta") {
1488
+ this.text.push(String(event.text || ""));
1489
+ }
1490
+ else if (kind === "stream_end") {
1491
+ const messageId = String(event.message_id || "");
1492
+ const block = this.text.join("");
1493
+ this.text = [];
1494
+ if (messageId) {
1495
+ this.blocks.set(messageId, block);
1496
+ if (event.phase === "answer")
1497
+ this.answerIds.push(messageId);
1498
+ }
1499
+ }
1500
+ else if (kind === "tool_call") {
1501
+ const callId = String(event.call_id || "");
1502
+ const args = mapping(event.args);
1503
+ const name = String(event.name || "");
1504
+ this.tools.set(callId, { name, callId, summary: String(event.summary || ""), args });
1505
+ if (name === "bash" && session.verifyCommand && sameCommand(String(args.command || ""), session.verifyCommand)) {
1506
+ this.verifyCalls.add(callId);
1507
+ }
1508
+ }
1509
+ else if (kind === "tool_result") {
1510
+ const callId = String(event.call_id || "");
1511
+ const record = this.tools.get(callId) || { name: String(event.name || ""), callId };
1512
+ let output = event.output;
1513
+ if (output === undefined || output === null)
1514
+ output = event.result || event.content || "";
1515
+ let text = typeof output === "string" ? output : JSON.stringify(output);
1516
+ if (!text && record.summary)
1517
+ text = record.summary;
1518
+ const updated = {
1519
+ name: record.name || String(event.name || ""), callId, summary: record.summary, output: text,
1520
+ isError: Boolean(event.is_error), isDiff: Boolean(event.is_diff),
1521
+ diff: typeof event.diff === "string" ? event.diff : undefined, args: record.args,
1522
+ };
1523
+ this.tools.set(callId, updated);
1524
+ if (this.verifyCalls.has(callId)) {
1525
+ const code = exitCode(text);
1526
+ const ok = code === null ? !updated.isError : code === 0;
1527
+ this.verifyModel = { ok, command: session.verifyCommand, output: text.slice(0, 8000), exitCode: code };
1528
+ this.verifyOrder += 1;
1529
+ this.verifyModelOrder = this.verifyOrder;
1530
+ }
1531
+ if (updated.name === "present_document") {
1532
+ (text.match(LOOPBACK) || []).forEach((url, index) => {
1533
+ const id = `${callId || "doc"}-${index}`;
1534
+ this.documents.set(id, { id, name: index === 0 ? "document" : "document.md", url });
1535
+ });
1536
+ }
1537
+ }
1538
+ else if (kind === "tool_denied") {
1539
+ const callId = String(event.call_id || "");
1540
+ const name = String(event.name || "");
1541
+ const reason = String(event.reason || "denied");
1542
+ this.tools.set(callId, { name, callId, output: reason, isError: true });
1543
+ this.denials.push({ name, reason, source: denialSource(reason), callId, args: mapping(event.args) });
1544
+ }
1545
+ else if (kind === "artifact_ready") {
1546
+ const art = {
1547
+ id: String(event.id || ""), name: String(event.name || ""), url: String(event.url || ""), rel: String(event.rel || ""),
1548
+ };
1549
+ this.artifacts.set(art.id, art);
1550
+ if (art.url.startsWith("http://127.0.0.1") || art.url.startsWith("http://localhost"))
1551
+ this.documents.set(art.id, art);
1552
+ }
1553
+ else if (kind === "todos") {
1554
+ this.tasks = session.projectTasks(event.todos);
1555
+ }
1556
+ else if (kind === "agent_started" || kind === "agent_ended") {
1557
+ const info = agentFromRow(event);
1558
+ if (info.id)
1559
+ this.agents.set(info.id, info);
1560
+ }
1561
+ else if (kind === "context") {
1562
+ if (typeof event.used === "number")
1563
+ this.usage.context_used = event.used;
1564
+ if (typeof event.size === "number")
1565
+ this.usage.context_size = event.size;
1566
+ }
1567
+ else if (kind === "info") {
1568
+ this.noteVerifier(String(event.message || ""), session.verifyCommand);
1569
+ }
1570
+ else if (kind === "error") {
1571
+ const message = String(event.message || "");
1572
+ if (message)
1573
+ this.lastError = message;
1574
+ this.noteVerifier(message, session.verifyCommand);
1575
+ }
1576
+ }
1577
+ noteVerifier(message, command) {
1578
+ if (!command || !message)
1579
+ return;
1580
+ const text = message.trim();
1581
+ if (text.startsWith(VERIFY_STARTED)) {
1582
+ this.verifyOrder += 1;
1583
+ this.verifyCliOrder = this.verifyOrder;
1584
+ this.verifyCli = "ran";
1585
+ this.verifyCliMessage = "";
1586
+ }
1587
+ else if (text.includes("configured verifier") && this.verifyCli
1588
+ && ["failed", "still failing", "did not pass"].some((word) => text.includes(word))) {
1589
+ this.verifyCli = "failed";
1590
+ this.verifyCliMessage = text;
1591
+ }
1592
+ }
1593
+ endTurn(event) {
1594
+ const reason = String(event.reason || "completed");
1595
+ this.reason = reason;
1596
+ const finalId = event.final_message_id;
1597
+ let final;
1598
+ if (typeof finalId === "string" && this.blocks.has(finalId))
1599
+ final = this.blocks.get(finalId) || "";
1600
+ else if (this.answerIds.length && this.blocks.has(this.answerIds[this.answerIds.length - 1])) {
1601
+ final = this.blocks.get(this.answerIds[this.answerIds.length - 1]) || "";
1602
+ }
1603
+ else
1604
+ final = [...this.blocks.values()].join("") || this.text.join("");
1605
+ this.result.finalText = final;
1606
+ this.blocks.clear();
1607
+ this.answerIds = [];
1608
+ this.text = [];
1609
+ if (typeof event.token_estimate === "number")
1610
+ this.usage.token_estimate = event.token_estimate;
1611
+ if (reason === "error" || reason === "failed") {
1612
+ this.status = "failed";
1613
+ this.error = this.lastError || "the turn ended with an error";
1614
+ }
1615
+ else if (reason === "cancelled" || reason === "interrupted") {
1616
+ this.status = "cancelled";
1617
+ }
1618
+ else {
1619
+ this.status = "completed";
1620
+ }
1621
+ this.lastError = "";
1622
+ }
1623
+ fail(reason, message) {
1624
+ this.status = "failed";
1625
+ this.reason = reason;
1626
+ this.error = message;
1627
+ }
1628
+ cancel() {
1629
+ this.status = "cancelled";
1630
+ this.reason = "cancelled";
1631
+ }
1632
+ partial() {
1633
+ this.result.partialText = this.text.join("") || [...this.blocks.values()].join("");
1634
+ }
1635
+ /** A cancel or timeout whose turn never reported its end within the grace period. */
1636
+ stopped(run, timeoutMs) {
1637
+ this.partial();
1638
+ if (run.cancelReason === "timeout")
1639
+ this.fail("timeout", `run exceeded its ${timeoutMs || 0} ms timeout`);
1640
+ else if (run.cancelReason === "decision_failed")
1641
+ this.fail("decision_failed", run.decisionError || "a decision callback failed");
1642
+ else
1643
+ this.cancel();
1644
+ }
1645
+ transport(run, error) {
1646
+ this.partial();
1647
+ if (run.cancelReason === "cancelled")
1648
+ this.cancel();
1649
+ else
1650
+ this.fail("transport", error instanceof Error ? error.message : String(error));
1651
+ }
1652
+ fatalError(run, event) {
1653
+ this.partial();
1654
+ if (run.cancelReason === "cancelled")
1655
+ this.cancel();
1656
+ else
1657
+ this.fail("runtime", String(event.message || "fatal backend error"));
1658
+ }
1659
+ /** Fill the run's result and return its final status (the caller publishes it last). */
1660
+ settle(run) {
1661
+ const result = this.result;
1662
+ result.tools = [...this.tools.values()];
1663
+ result.denials = [...this.denials];
1664
+ result.artifacts = [...this.artifacts.values()];
1665
+ result.documents = [...this.documents.values()];
1666
+ result.tasks = this.tasks;
1667
+ result.agents = [...this.agents.values()];
1668
+ let status = this.status;
1669
+ let reason = this.reason;
1670
+ let error = this.error;
1671
+ if (run.cancelReason === "timeout") {
1672
+ status = "failed";
1673
+ reason = "timeout";
1674
+ error = error && error.includes("timeout") ? error
1675
+ : this.timeoutMs ? `run exceeded its ${this.timeoutMs} ms timeout` : "run exceeded its timeout";
1676
+ result.partialText = result.partialText || result.finalText;
1677
+ }
1678
+ else if (run.cancelReason === "decision_failed") {
1679
+ status = "failed";
1680
+ reason = "decision_failed";
1681
+ error = run.decisionError || "a decision callback failed";
1682
+ result.partialText = result.partialText || result.finalText;
1683
+ }
1684
+ else if (run.cancelReason === "cancelled" || status === "cancelled") {
1685
+ status = "cancelled";
1686
+ reason = "cancelled";
1687
+ error = undefined;
1688
+ result.partialText = result.partialText || result.finalText;
1689
+ }
1690
+ else if (status === "running") {
1691
+ status = "failed";
1692
+ reason = reason || "error";
1693
+ error = error || "the run ended without a result";
1694
+ }
1695
+ result.error = error;
1696
+ result.reason = reason || status;
1697
+ result.usage = { ...this.usage };
1698
+ return status;
1699
+ }
1700
+ verification(command) {
1701
+ const text = (command || "").trim();
1702
+ if (!text)
1703
+ return undefined;
1704
+ if (this.verifyCli && this.verifyCliOrder > this.verifyModelOrder) {
1705
+ if (this.verifyCli === "failed")
1706
+ return { ok: false, command: text, output: this.verifyCliMessage };
1707
+ if (this.status === "completed" && this.reason === "completed")
1708
+ return { ok: true, command: text, exitCode: 0 };
1709
+ return { ok: null, command: text };
1710
+ }
1711
+ if (this.verifyModel)
1712
+ return this.verifyModel;
1713
+ return { ok: null, command: text };
1714
+ }
1715
+ }
1716
+ function permissionRows(raw) {
1717
+ return (Array.isArray(raw) ? raw : []).filter((row) => row && typeof row === "object").map((row) => {
1718
+ const item = row;
1719
+ return { action: String(item.action || ""), rule: String(item.rule || "") };
1720
+ });
1721
+ }
1722
+ export function rowsToSessions(raw) {
1723
+ const items = [];
1724
+ for (const row of Array.isArray(raw) ? raw : []) {
1725
+ if (!row || typeof row !== "object")
1726
+ continue;
1727
+ const item = row;
1728
+ const path = String(item.path || "");
1729
+ items.push({
1730
+ id: path ? basename(path).replace(/\.json$/, "") : "",
1731
+ path,
1732
+ name: String(item.name || ""),
1733
+ preview: String(item.preview || ""),
1734
+ messageCount: Number(item.count || 0),
1735
+ when: String(item.when || ""),
1736
+ });
1737
+ }
1738
+ return items;
1739
+ }