dsh-agy-link 0.4.49 → 0.4.53

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/index.js CHANGED
@@ -457,6 +457,7 @@ const AUTH_REVERIFY_DELAY_MS = 6e5;
457
457
  * simply re-stamped.
458
458
  */
459
459
  function needsAuthReverify(account, now = Date.now()) {
460
+ if (!account.enabled) return false;
460
461
  if (!account.authRequired) return false;
461
462
  if (typeof account.authMarkedAt !== "number") return true;
462
463
  return now - account.authMarkedAt > AUTH_REVERIFY_DELAY_MS;
@@ -1348,6 +1349,158 @@ function defineAgyMirrorTool(deps) {
1348
1349
  });
1349
1350
  }
1350
1351
  //#endregion
1352
+ //#region src/host/dsh-goal.ts
1353
+ const toToolCallId$1 = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1354
+ function extractText$1(m) {
1355
+ const content = m.content;
1356
+ if (typeof content === "string") return content;
1357
+ if (Array.isArray(content)) {
1358
+ const parts = [];
1359
+ for (const b of content) if (b && typeof b === "object") {
1360
+ const blk = b;
1361
+ if (blk.type === "text" && typeof blk.text === "string") parts.push(blk.text);
1362
+ }
1363
+ return parts.join("\n");
1364
+ }
1365
+ return "";
1366
+ }
1367
+ function parseGoalRoundText(text) {
1368
+ if (!text.includes("<goal_round>")) return null;
1369
+ const roundMatch = text.match(/Round:\s*(\d+)(?:\/(\d+))?/);
1370
+ const round = roundMatch ? parseInt(roundMatch[1], 10) : void 0;
1371
+ const maxRounds = roundMatch && roundMatch[2] ? parseInt(roundMatch[2], 10) : void 0;
1372
+ const objMatch = text.match(/Objective:\s*(?:"([^"]*)"|'([^']*)'|([^\n]+))/);
1373
+ const objective = objMatch ? (objMatch[1] || objMatch[2] || objMatch[3])?.trim() : void 0;
1374
+ const idMatch = text.match(/goal-[a-zA-Z0-9_-]+/);
1375
+ return {
1376
+ goalId: idMatch ? idMatch[0] : void 0,
1377
+ round,
1378
+ maxRounds,
1379
+ objective
1380
+ };
1381
+ }
1382
+ /**
1383
+ * Detect whether the incoming request is part of a DSH <goal_round> continuation.
1384
+ * Extracts goalId, revision, round, and objective if available.
1385
+ */
1386
+ function detectGoalContext(messages) {
1387
+ let goalId;
1388
+ let revision;
1389
+ let round;
1390
+ let maxRounds;
1391
+ let objective;
1392
+ let isGoal = false;
1393
+ for (let i = messages.length - 1; i >= 0; i--) {
1394
+ const m = messages[i];
1395
+ if (!m) continue;
1396
+ const src = m.source;
1397
+ if (src && typeof src === "object" && src.kind === "goal") {
1398
+ isGoal = true;
1399
+ if (!goalId && typeof src.goalId === "string") goalId = src.goalId;
1400
+ if (revision === void 0 && typeof src.revision === "number") revision = src.revision;
1401
+ if (round === void 0 && typeof src.round === "number") round = src.round;
1402
+ }
1403
+ const text = extractText$1(m);
1404
+ if (text.includes("<goal_round>")) {
1405
+ isGoal = true;
1406
+ const parsed = parseGoalRoundText(text);
1407
+ if (parsed) {
1408
+ if (!goalId && parsed.goalId) goalId = parsed.goalId;
1409
+ if (revision === void 0 && parsed.revision !== void 0) revision = parsed.revision;
1410
+ if (round === void 0 && parsed.round !== void 0) round = parsed.round;
1411
+ if (maxRounds === void 0 && parsed.maxRounds !== void 0) maxRounds = parsed.maxRounds;
1412
+ if (!objective && parsed.objective) objective = parsed.objective;
1413
+ }
1414
+ }
1415
+ }
1416
+ if (isGoal) return {
1417
+ goalId,
1418
+ revision: revision ?? 1,
1419
+ round,
1420
+ maxRounds,
1421
+ objective
1422
+ };
1423
+ return null;
1424
+ }
1425
+ /**
1426
+ * Detect whether DSH is in Plan Mode.
1427
+ * DSH plan-mode injects "You are in plan mode" into the runtime context prompt.
1428
+ */
1429
+ function isPlanMode(messages) {
1430
+ for (let i = messages.length - 1; i >= 0; i--) {
1431
+ const m = messages[i];
1432
+ if (!m) continue;
1433
+ if (extractText$1(m).includes("You are in plan mode")) return true;
1434
+ }
1435
+ return false;
1436
+ }
1437
+ /**
1438
+ * Check if the trailing message is the tool result of a bridge-emitted `update_goal` call.
1439
+ */
1440
+ function isGoalToolContinuation(messages) {
1441
+ if (messages.length === 0) return false;
1442
+ const last = messages[messages.length - 1];
1443
+ if (!last) return false;
1444
+ const callId = last.toolCallId ?? last.source?.callId;
1445
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-goal-")) return true;
1446
+ return false;
1447
+ }
1448
+ /**
1449
+ * Check if the trailing message is the tool result of a bridge-emitted `exit_plan_mode` call.
1450
+ */
1451
+ function isPlanToolContinuation(messages) {
1452
+ if (messages.length === 0) return false;
1453
+ const last = messages[messages.length - 1];
1454
+ if (!last) return false;
1455
+ const callId = last.toolCallId ?? last.source?.callId;
1456
+ if (typeof callId === "string" && callId.startsWith("agytc-bridge-plan-")) return true;
1457
+ return false;
1458
+ }
1459
+ /**
1460
+ * Build the synthetic `update_goal` tool-call chunk that marks the goal complete on DSH.
1461
+ */
1462
+ function buildGoalCompleteToolCallBlock(goalContext, blockIdx) {
1463
+ const goalId = goalContext.goalId || "active-goal";
1464
+ const revision = goalContext.revision ?? 1;
1465
+ return [{
1466
+ type: "block-start",
1467
+ index: blockIdx,
1468
+ blockType: "tool-call"
1469
+ }, {
1470
+ type: "block-end",
1471
+ index: blockIdx,
1472
+ block: {
1473
+ type: "tool-call",
1474
+ id: toToolCallId$1(`agytc-bridge-goal-${goalId}-${revision}`),
1475
+ name: "update_goal",
1476
+ arguments: JSON.stringify({
1477
+ goal_id: goalId,
1478
+ revision,
1479
+ action: "complete"
1480
+ })
1481
+ }
1482
+ }];
1483
+ }
1484
+ /**
1485
+ * Build the synthetic `exit_plan_mode` tool-call chunk that presents the generated plan to DSH.
1486
+ */
1487
+ function buildPlanExitToolCallBlock(planMarkdown, blockIdx) {
1488
+ return [{
1489
+ type: "block-start",
1490
+ index: blockIdx,
1491
+ blockType: "tool-call"
1492
+ }, {
1493
+ type: "block-end",
1494
+ index: blockIdx,
1495
+ block: {
1496
+ type: "tool-call",
1497
+ id: toToolCallId$1(`agytc-bridge-plan-${Date.now()}`),
1498
+ name: "exit_plan_mode",
1499
+ arguments: JSON.stringify({ plan: planMarkdown.trim() !== "" ? planMarkdown : "# Plan Completed\n\nPlan generated and ready for review." })
1500
+ }
1501
+ }];
1502
+ }
1503
+ //#endregion
1351
1504
  //#region src/host/mapper.ts
1352
1505
  const toToolCallId = dshLlm.ToolCallId ?? dshLlm.CallId ?? ((id) => id);
1353
1506
  function usageFromRaw(raw) {
@@ -1396,6 +1549,7 @@ var EventMapper = class {
1396
1549
  bannerOnlyThinkingEmitted = false;
1397
1550
  sawTextStep;
1398
1551
  finished = false;
1552
+ totalTextOutput = "";
1399
1553
  constructor(opts) {
1400
1554
  this.opts = opts;
1401
1555
  this.sawTextStep = opts.initialSawText === true;
@@ -1438,6 +1592,7 @@ var EventMapper = class {
1438
1592
  appendDelta(delta) {
1439
1593
  if (delta === "") return null;
1440
1594
  this.openAcc += delta;
1595
+ if (this.openType === "text") this.totalTextOutput += delta;
1441
1596
  return this.openType === "text" ? {
1442
1597
  type: "text-delta",
1443
1598
  index: this.blockIdx,
@@ -1449,10 +1604,10 @@ var EventMapper = class {
1449
1604
  };
1450
1605
  }
1451
1606
  /**
1452
- * Emit thinking: always retain the [agy thinking turn · ... thinking tokens]
1453
- * banner. When real thought prose is present, place the banner and thought
1607
+ * Emit thinking: always retain the [agy · ... tokens] banner.
1608
+ * When real thought prose is present, place the banner and thought
1454
1609
  * text on the same initial line so DSH's collapsed summary renders:
1455
- * [agy thinking turn · *** thinking tokens] [Chain-of-Thought body]
1610
+ * [agy · *** tokens] [Chain-of-Thought body]
1456
1611
  *
1457
1612
  * With `allowBannerOnly` the bare banner is emitted when the DB prose never
1458
1613
  * arrived (desktop hosts without any sqlite reader, issue #43): the user
@@ -1466,7 +1621,7 @@ var EventMapper = class {
1466
1621
  const bannerAlreadyShown = this.bannerOnlyShown.has(stepKey);
1467
1622
  if (hasText) this.bannerOnlyShown.delete(stepKey);
1468
1623
  yield* this.ensureBlock("reasoning");
1469
- const banner = thoughtTokens > 0 ? "[agy thinking turn · " + thoughtTokens + " thinking tokens]" : "[agy thinking turn]";
1624
+ const banner = thoughtTokens > 0 ? "[agy · " + thoughtTokens + " tokens]" : "[agy]";
1470
1625
  const combined = !hasText ? `${banner}\n` : bannerAlreadyShown ? `${text.trim()}\n` : `${banner} ${text.trim()}\n`;
1471
1626
  const d = this.appendDelta(combined);
1472
1627
  if (d) yield d;
@@ -1650,6 +1805,28 @@ var EventMapper = class {
1650
1805
  type: "usage",
1651
1806
  usage: usageFromRaw(this.opts.usage?.finalUsage(ev.usage) ?? ev.usage)
1652
1807
  };
1808
+ if (this.opts.supportsGoalUpdate && this.opts.goalContext?.goalId) {
1809
+ for (const ch of buildGoalCompleteToolCallBlock(this.opts.goalContext, this.blockIdx)) yield ch;
1810
+ this.blockIdx++;
1811
+ yield {
1812
+ type: "finish",
1813
+ reason: { kind: "tool-calls" },
1814
+ ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1815
+ };
1816
+ this.finished = true;
1817
+ return;
1818
+ }
1819
+ if (this.opts.supportsPlanExit && this.totalTextOutput.trim() !== "") {
1820
+ for (const ch of buildPlanExitToolCallBlock(this.totalTextOutput, this.blockIdx)) yield ch;
1821
+ this.blockIdx++;
1822
+ yield {
1823
+ type: "finish",
1824
+ reason: { kind: "tool-calls" },
1825
+ ...ev.conversationId !== "" ? { replayState: { response: { conversationId: ev.conversationId } } } : {}
1826
+ };
1827
+ this.finished = true;
1828
+ return;
1829
+ }
1653
1830
  yield {
1654
1831
  type: "finish",
1655
1832
  reason: { kind: "stop" },
@@ -2291,24 +2468,25 @@ var StreamJsonParser = class {
2291
2468
  const ev = this.takeLine(rest);
2292
2469
  return ev ? [ev] : [];
2293
2470
  }
2471
+ recordAuthFailure(text) {
2472
+ this.stats.sawAuthFailure = true;
2473
+ const prev = this.stats.authFailureText ?? "";
2474
+ if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + text.trim().slice(0, 300);
2475
+ }
2294
2476
  takeLine(line) {
2295
2477
  if (line.trim() === "") return void 0;
2296
2478
  this.stats.lines++;
2297
2479
  this.recentLines.push(line);
2298
2480
  if (this.recentLines.length > this.maxRecent) this.recentLines.splice(0, this.recentLines.length - this.maxRecent);
2299
- if (looksLikeAuthFailure(line)) {
2300
- this.stats.sawAuthFailure = true;
2301
- const prev = this.stats.authFailureText ?? "";
2302
- if (prev.length < 600) this.stats.authFailureText = (prev === "" ? "" : prev + " ") + line.trim().slice(0, 300);
2303
- }
2304
- if (this.stats.authUrl === void 0) {
2305
- const u = extractAuthUrl(line);
2306
- if (u) this.stats.authUrl = u;
2307
- }
2308
2481
  let obj;
2309
2482
  try {
2310
2483
  obj = JSON.parse(line);
2311
2484
  } catch {
2485
+ if (looksLikeAuthFailure(line)) this.recordAuthFailure(line);
2486
+ if (this.stats.authUrl === void 0) {
2487
+ const u = extractAuthUrl(line);
2488
+ if (u) this.stats.authUrl = u;
2489
+ }
2312
2490
  this.stats.garbage++;
2313
2491
  this.stats.consecutiveGarbage++;
2314
2492
  return {
@@ -2325,7 +2503,10 @@ var StreamJsonParser = class {
2325
2503
  line
2326
2504
  };
2327
2505
  }
2328
- if (ev.kind === "result") this.stats.lastResultError = ev.error ?? void 0;
2506
+ if (ev.kind === "result") {
2507
+ this.stats.lastResultError = ev.error ?? void 0;
2508
+ if (!ev.ok && ev.error && looksLikeAuthFailure(ev.error)) this.recordAuthFailure(ev.error);
2509
+ }
2329
2510
  this.stats.consecutiveGarbage = 0;
2330
2511
  return ev;
2331
2512
  }
@@ -2735,6 +2916,20 @@ function buildStreamInputLine(prompt) {
2735
2916
  }
2736
2917
  }) + "\n";
2737
2918
  }
2919
+ /**
2920
+ * Remove NUL characters from text destined for a child process.
2921
+ *
2922
+ * `spawn()` rejects any argv element containing U+0000 with
2923
+ * `ERR_INVALID_ARG_VALUE: The argument 'args[N]' must be a string without null
2924
+ * bytes`, which aborts the whole turn before agy is even started. NULs reach the
2925
+ * assembled prompt through binary tool output (a tool result that quoted a
2926
+ * binary file — `.git` objects, packed refs, images, …), so every prompt that
2927
+ * can carry tool text passes through here. Other control characters are legal in
2928
+ * argv and are left untouched.
2929
+ */
2930
+ function stripNulBytes(text) {
2931
+ return text.includes("\0") ? text.split("\0").join("") : text;
2932
+ }
2738
2933
  /** Whether the assembled argv (including a trailing `-p <prompt>`) risks ENAMETOOLONG. */
2739
2934
  function shouldUsePromptStdin(argsWithPrompt) {
2740
2935
  let size = 0;
@@ -3497,6 +3692,21 @@ var AccountPoolManager = class {
3497
3692
  this.data.lastActiveAccountId = id;
3498
3693
  this.persist();
3499
3694
  }
3695
+ /**
3696
+ * Manually pin the active slot for one family (console "设为当前使用").
3697
+ * The pin survives until the scheduler legitimately rotates away (quota
3698
+ * exhaustion / cooldown / auth flag) — getActiveAccountId still enforces
3699
+ * eligibility, so pinning an ineligible slot simply does not stick.
3700
+ */
3701
+ setActiveAccount(id, family = "google") {
3702
+ const acc = this.getAccount(id);
3703
+ if (!acc || !acc.enabled || acc.authRequired) return false;
3704
+ if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
3705
+ this.data.activeAccountIds[family] = id;
3706
+ this.data.lastActiveAccountId = id;
3707
+ this.persist();
3708
+ return true;
3709
+ }
3500
3710
  markAuthRequired(id, reason) {
3501
3711
  const acc = this.getAccount(id);
3502
3712
  if (!acc) return;
@@ -3548,9 +3758,11 @@ var AccountPoolManager = class {
3548
3758
  setPrimaryAccount(id) {
3549
3759
  const idx = this.data.accounts.findIndex((a) => a.id === id);
3550
3760
  if (idx === -1) return false;
3551
- this.data.primaryAccountId = id;
3552
3761
  const [acc] = this.data.accounts.splice(idx, 1);
3553
- if (acc) this.data.accounts.unshift(acc);
3762
+ if (!acc) return false;
3763
+ acc.enabled = true;
3764
+ this.data.primaryAccountId = id;
3765
+ this.data.accounts.unshift(acc);
3554
3766
  this.data.activeAccountIds = {
3555
3767
  google: id,
3556
3768
  anthropic: id,
@@ -3598,7 +3810,7 @@ var AccountPoolManager = class {
3598
3810
  if (quota && typeof quota.weeklyFraction === "number" && quota.weeklyFraction <= .01) {
3599
3811
  if (quota.weeklyResetTime) {
3600
3812
  const resetMs = Date.parse(quota.weeklyResetTime);
3601
- if (!Number.isNaN(resetMs) && resetMs > now) return false;
3813
+ if (Number.isNaN(resetMs) || resetMs > now) return false;
3602
3814
  } else return false;
3603
3815
  }
3604
3816
  return true;
@@ -3617,7 +3829,7 @@ var AccountPoolManager = class {
3617
3829
  }
3618
3830
  const next = this.selectAccount(family);
3619
3831
  if (next) return next.id;
3620
- return this.data.primaryAccountId ?? this.data.accounts[0]?.id;
3832
+ return this.data.accounts.find((a) => a.id === this.data.primaryAccountId && a.enabled)?.id ?? this.data.accounts.find((a) => a.enabled)?.id;
3621
3833
  }
3622
3834
  /**
3623
3835
  * Next moment this account's quota state refreshes for one family: the
@@ -3639,23 +3851,34 @@ var AccountPoolManager = class {
3639
3851
  return (idx - start + total) % total;
3640
3852
  }
3641
3853
  /**
3642
- * "Use it or lose it" rotation order. When the sticky account is drained we
3643
- * prefer the eligible account whose quota window refreshes SOONEST: residual
3644
- * quota disappears at that reset, so draining it first is free capacity the
3645
- * pool would otherwise throw away. Accounts with no quota metadata yet keep
3646
- * the legacy cyclic order (which also keeps a drained slot from bouncing
3647
- * straight back to the primary).
3854
+ * "Use it or lose it" rotation order.
3855
+ * Priority 1: Weekly quota expiring soonest (least remaining time until weeklyResetTime).
3856
+ * Priority 2: 5-hour quota expiring soonest (least remaining time until resetTime).
3857
+ * Priority 3: Cyclic distance from `fromId` (or pool order).
3648
3858
  */
3649
3859
  orderByExpiry(candidates, family, fromId) {
3650
- return candidates.map((acc, index) => ({
3651
- acc,
3652
- index,
3653
- at: this.nextQuotaResetAt(acc, family)
3654
- })).sort((a, b) => {
3655
- const aKnown = !Number.isNaN(a.at);
3656
- const bKnown = !Number.isNaN(b.at);
3657
- if (aKnown !== bKnown) return aKnown ? -1 : 1;
3658
- if (aKnown && bKnown && a.at !== b.at) return a.at - b.at;
3860
+ const now = Date.now();
3861
+ return candidates.map((acc, index) => {
3862
+ const q = acc.quotas?.[family];
3863
+ const weeklyMs = q?.weeklyResetTime ? Date.parse(q.weeklyResetTime) : NaN;
3864
+ const fiveHourMs = q?.resetTime ? Date.parse(q.resetTime) : NaN;
3865
+ const weeklyRemaining = !Number.isNaN(weeklyMs) ? Math.max(0, weeklyMs - now) : Number.POSITIVE_INFINITY;
3866
+ const fiveHourRemaining = !Number.isNaN(fiveHourMs) ? Math.max(0, fiveHourMs - now) : Number.POSITIVE_INFINITY;
3867
+ return {
3868
+ acc,
3869
+ index,
3870
+ weeklyRemaining,
3871
+ fiveHourRemaining,
3872
+ weeklyKnown: Number.isFinite(weeklyRemaining),
3873
+ fiveHourKnown: Number.isFinite(fiveHourRemaining)
3874
+ };
3875
+ }).sort((a, b) => {
3876
+ if (a.weeklyKnown && b.weeklyKnown) {
3877
+ if (a.weeklyRemaining !== b.weeklyRemaining) return a.weeklyRemaining - b.weeklyRemaining;
3878
+ } else if (a.weeklyKnown !== b.weeklyKnown) return a.weeklyKnown ? -1 : 1;
3879
+ if (a.fiveHourKnown && b.fiveHourKnown) {
3880
+ if (a.fiveHourRemaining !== b.fiveHourRemaining) return a.fiveHourRemaining - b.fiveHourRemaining;
3881
+ } else if (a.fiveHourKnown !== b.fiveHourKnown) return a.fiveHourKnown ? -1 : 1;
3659
3882
  const da = this.cyclicDistance(fromId, a.acc.id);
3660
3883
  const db = this.cyclicDistance(fromId, b.acc.id);
3661
3884
  return da !== db ? da - db : a.index - b.index;
@@ -3781,7 +4004,7 @@ var AccountPoolManager = class {
3781
4004
  return activeCandidate;
3782
4005
  }
3783
4006
  }
3784
- const nextAccount = activeId !== void 0 ? this.orderByExpiry(candidates, family, activeId)[0] : candidates[0];
4007
+ const nextAccount = this.orderByExpiry(candidates, family, activeId)[0];
3785
4008
  if (!this.data.activeAccountIds) this.data.activeAccountIds = {};
3786
4009
  this.data.activeAccountIds[family] = nextAccount.id;
3787
4010
  this.data.lastActiveAccountId = nextAccount.id;
@@ -28739,14 +28962,14 @@ function mergeSentImageIds(existing, added, max = 64) {
28739
28962
  * budget is filled from the newest other turns.
28740
28963
  */
28741
28964
  function buildDigest(messages, fromIdx, maxChars) {
28742
- const taskText = latestUserTaskText(messages.slice(fromIdx));
28965
+ const taskText = stripNulBytes(latestUserTaskText(messages.slice(fromIdx)));
28743
28966
  const taskLine = taskText !== "" ? "User: " + taskText : "";
28744
28967
  const otherLines = [];
28745
28968
  for (let i = fromIdx; i < messages.length; i++) {
28746
28969
  const m = messages[i];
28747
28970
  if (m === void 0 || m.role === "system") continue;
28748
28971
  if (isSyntheticHarnessMessage(m)) continue;
28749
- const text = textOf(m);
28972
+ const text = stripNulBytes(textOf(m));
28750
28973
  if (text === "") continue;
28751
28974
  if (taskLine !== "" && !isToolResultMessage(m) && m.role === "user" && text === taskText) continue;
28752
28975
  otherLines.push(digestLabel(m) + text);
@@ -28809,7 +29032,9 @@ function brief(s) {
28809
29032
  }
28810
29033
  function sawAuthFailure(parser, outcome) {
28811
29034
  if (parser.stats.sawAuthFailure) return true;
28812
- return looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3));
29035
+ if (looksLikeAuthFailure(outcome.stderrTail)) return true;
29036
+ if (parser.stats.lastResultError && looksLikeAuthFailure(parser.stats.lastResultError)) return true;
29037
+ return false;
28813
29038
  }
28814
29039
  /**
28815
29040
  * Bounded wait for an aborted run to settle, so a steer replacement never
@@ -29068,12 +29293,29 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29068
29293
  this.warnOnce("cwd:" + (sessionKey || "anon"), "session workspace unresolved (sessionId=" + (sessionKey || "none") + ") — running agy in the DSH process cwd: " + workspaceRoot);
29069
29294
  }
29070
29295
  }
29296
+ if (isGoalToolContinuation(options.messages) || isPlanToolContinuation(options.messages)) {
29297
+ yield {
29298
+ type: "finish",
29299
+ reason: { kind: "stop" }
29300
+ };
29301
+ return;
29302
+ }
29303
+ const goalContext = detectGoalContext(options.messages);
29304
+ if (goalContext && (!goalContext.goalId || goalContext.revision === void 0) && sessionKey && this.deps.sessionGoal) {
29305
+ const sg = this.deps.sessionGoal(sessionKey);
29306
+ if (sg) {
29307
+ if (!goalContext.goalId) goalContext.goalId = sg.goalId;
29308
+ if (goalContext.revision === void 0) goalContext.revision = sg.revision;
29309
+ }
29310
+ }
29311
+ const supportsGoalUpdate = Boolean(options.tools?.some((t) => t.name === "update_goal"));
29312
+ const supportsPlanExit = Boolean(options.tools?.some((t) => t.name === "exit_plan_mode")) && isPlanMode(options.messages);
29071
29313
  const continuation = detectContinuation(options.messages);
29072
29314
  if (continuation !== null) {
29073
29315
  const rec = this.deps.runs.get(continuation.runId);
29074
29316
  const deadFailure = rec !== void 0 && rec.isSettled ? rec.failureInfo : null;
29075
29317
  if (rec !== void 0 && !(deadFailure !== null && this.claimFailover(sessionKey, deadFailure.code))) {
29076
- yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode);
29318
+ yield* this.driveSpan(rec, continuation.eventIndex + 1, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
29077
29319
  return;
29078
29320
  }
29079
29321
  if (rec !== void 0) {
@@ -29163,7 +29405,9 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29163
29405
  }
29164
29406
  if (!account) {
29165
29407
  const pool = this.deps.pool;
29166
- const accounts = pool.getAccounts().filter((a) => a.enabled);
29408
+ const allAccounts = pool.getAccounts();
29409
+ const accounts = allAccounts.filter((a) => a.enabled);
29410
+ const disabled = allAccounts.length - accounts.length;
29167
29411
  const cooling = accounts.filter((a) => pool.isAccountEligible(a, family) === false && !a.authRequired).length;
29168
29412
  const signedOut = accounts.filter((a) => a.authRequired === true).length;
29169
29413
  const countdown = pool.getEarliestAvailableCountdown?.(family) ?? pool.getEarliestResetCountdown(family);
@@ -29172,7 +29416,17 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29172
29416
  account = pool.selectAccount(family) ?? void 0;
29173
29417
  }
29174
29418
  if (!account) {
29175
- const why = signedOut > 0 && signedOut === accounts.length ? "every account needs a re-login (run /agy auth)" : `no account can serve ${family} right now (${cooling} in cooldown, ${signedOut} signed out)`;
29419
+ let why;
29420
+ if (accounts.length === 0 && allAccounts.length > 0) why = "all accounts are disabled (enable an account in the console or run /agy enable-account)";
29421
+ else if (signedOut > 0 && signedOut === accounts.length) why = "every account needs a re-login (run /agy auth)";
29422
+ else {
29423
+ const details = [];
29424
+ if (cooling > 0) details.push(`${cooling} in cooldown`);
29425
+ if (signedOut > 0) details.push(`${signedOut} signed out`);
29426
+ if (disabled > 0) details.push(`${disabled} disabled`);
29427
+ const detailStr = details.length > 0 ? ` (${details.join(", ")})` : "";
29428
+ why = `no account can serve ${family} right now${detailStr}`;
29429
+ }
29176
29430
  const waitStr = countdown !== null && countdown > 0 ? ` (earliest reset in ${Math.ceil(countdown / 1e3)}s)` : "";
29177
29431
  throw new LlmError(`All Antigravity accounts are unavailable: ${why}${waitStr}. Add an account, wait for reset, or run /agy clear-cooldown.`, Err.AGY_ERROR);
29178
29432
  }
@@ -29294,7 +29548,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29294
29548
  startedAt: now,
29295
29549
  inFlight: true
29296
29550
  });
29297
- yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode);
29551
+ yield* this.driveSpan(liveRun, rescueFrom, hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
29298
29552
  return;
29299
29553
  }
29300
29554
  throw new LlmError("Duplicate request ignored: an identical request is already running for this session.", Err.BUSY);
@@ -29379,6 +29633,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29379
29633
  };
29380
29634
  const activeModelForArgs = activeModel === "" ? cfg.defaultModel : activeModel;
29381
29635
  const effectiveAddDirs = workspaceRoot !== "" ? stagedDirs.includes(workspaceRoot) ? stagedDirs : [workspaceRoot, ...stagedDirs] : stagedDirs;
29636
+ prompt = stripNulBytes(prompt);
29382
29637
  const argsBase = this.buildArgs({
29383
29638
  prompt,
29384
29639
  model: activeModelForArgs,
@@ -29544,31 +29799,31 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29544
29799
  code: Err.TIMEOUT,
29545
29800
  message: "agy run was idle for " + cfg.timeoutMs + "ms without output" + (silentTimeout ? " — agy never emitted a single event; it is likely unable to reach Google (check proxy/network). 无法连接 Google,请检查代理或网络配置" : "")
29546
29801
  };
29547
- else if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
29548
- kind: "error",
29549
- code: Err.NETWORK,
29550
- message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
29551
- } : {
29552
- kind: "error",
29553
- code: Err.AUTH,
29554
- message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
29555
- };
29556
- else if (isRateLimit) {
29557
- const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
29558
- failure = {
29802
+ else if (!consumable) {
29803
+ if (sawAuthFailure(parser, outcome)) failure = transportFailure ? {
29559
29804
  kind: "error",
29560
- code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
29561
- message: "Google Antigravity quota / rate limit reached: " + bestMsg
29562
- };
29563
- } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
29564
- const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
29565
- failure = {
29805
+ code: Err.NETWORK,
29806
+ message: "agy could not validate its Google login over the network (transport failure, not a sign-out) — retrying on another account. " + (rawErrText !== "" ? brief(rawErrText) : "")
29807
+ } : {
29566
29808
  kind: "error",
29567
- code: Err.ELIGIBILITY,
29568
- message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
29809
+ code: Err.AUTH,
29810
+ message: "agy is not signed in — run /agy auth (or run agy once in a terminal) to login"
29569
29811
  };
29570
- } else if (!consumable) {
29571
- if (outcome.code !== 0) {
29812
+ else if (isRateLimit) {
29813
+ const bestMsg = parser.stats.lastResultError || (outcome.stderrTail ? brief(outcome.stderrTail) : "Rate limit or quota reached");
29814
+ failure = {
29815
+ kind: "error",
29816
+ code: looksLikeHardRateLimit(rawErrText) ? Err.RATE_LIMIT : Err.OVERLOADED,
29817
+ message: "Google Antigravity quota / rate limit reached: " + bestMsg
29818
+ };
29819
+ } else if (!transportFailure && (looksLikeEligibilityFailure(rawErrText) || looksLikeEligibilityFailure(parser.stats.lastResultError ?? ""))) {
29820
+ const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
29821
+ failure = {
29822
+ kind: "error",
29823
+ code: Err.ELIGIBILITY,
29824
+ message: ELIGIBILITY_ERROR_HINT + (detail !== "" ? " (" + detail + ")" : "")
29825
+ };
29826
+ } else if (outcome.code !== 0) {
29572
29827
  const detail = parser.stats.lastResultError ?? (outcome.stderrTail !== "" ? brief(outcome.stderrTail) : "");
29573
29828
  failure = transportFailure ? {
29574
29829
  kind: "error",
@@ -29648,7 +29903,7 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29648
29903
  message: "internal error: " + brief(String(err))
29649
29904
  });
29650
29905
  });
29651
- yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode);
29906
+ yield* this.driveSpan(rec, 0, !isAux && hasToolSupport, isCodeMode, goalContext, supportsGoalUpdate, supportsPlanExit);
29652
29907
  }
29653
29908
  /**
29654
29909
  * Stream one span of a recording: map events from `from` until the mapper
@@ -29657,18 +29912,18 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29657
29912
  * as this span's terminal chunk — the turn ends exactly like a native
29658
29913
  * provider error.
29659
29914
  */
29660
- async *driveSpan(rec, from, cutOnTool, useCodeWrapper) {
29915
+ async *driveSpan(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
29661
29916
  this.deps.runs.remember(rec);
29662
29917
  this.deps.runs.pin(rec.runId);
29663
29918
  try {
29664
- yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper);
29919
+ yield* this.driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit);
29665
29920
  } finally {
29666
29921
  this.deps.runs.unpin(rec.runId);
29667
29922
  this.deps.runs.remember(rec);
29668
29923
  }
29669
29924
  }
29670
29925
  /** Body of {@link driveSpan}: map recorded events until the span finishes. */
29671
- async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper) {
29926
+ async *driveSpanBody(rec, from, cutOnTool, useCodeWrapper, goalContext, supportsGoalUpdate, supportsPlanExit) {
29672
29927
  const queue = new ChunkQueue();
29673
29928
  (async () => {
29674
29929
  const resolved = /* @__PURE__ */ new Map();
@@ -29682,7 +29937,10 @@ var AgyAdapter = class AgyAdapter extends LlmAdapter {
29682
29937
  usage: rec,
29683
29938
  resolvedFullArgs: resolved,
29684
29939
  resolvedThoughts,
29685
- mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0
29940
+ mediaBaseUrl: this.deps.getMediaBaseUrl?.() ?? void 0,
29941
+ goalContext,
29942
+ supportsGoalUpdate,
29943
+ supportsPlanExit
29686
29944
  });
29687
29945
  let i = from;
29688
29946
  try {
@@ -29868,6 +30126,7 @@ async function runAgyOnce(deps, req) {
29868
30126
  if (req.effort) args.push("--effort", req.effort);
29869
30127
  let prompt = req.prompt;
29870
30128
  if (req.readPaths && req.readPaths.length > 0) prompt = await inlineFiles(prompt, req.readPaths, cfg.workspaceRoot !== "" ? cfg.workspaceRoot : process.cwd());
30129
+ prompt = stripNulBytes(prompt);
29871
30130
  let cleanup = null;
29872
30131
  if (req.schema !== void 0 && req.schema !== null) {
29873
30132
  const sa = await schemaArgs(req.schema);
@@ -29922,15 +30181,15 @@ async function runAgyOnce(deps, req) {
29922
30181
  error: "timed out after " + timeoutMs + "ms",
29923
30182
  durationMs: outcome.durationMs
29924
30183
  };
29925
- if (looksLikeAuthFailure(outcome.stderrTail) || looksLikeAuthFailure(outcome.stdout.slice(0, 4e3))) return {
30184
+ const joined = textParts.join("\n").trim();
30185
+ const text = joined !== "" ? joined : resultText;
30186
+ if (text === "" && (looksLikeAuthFailure(outcome.stderrTail) || parser.stats.sawAuthFailure)) return {
29926
30187
  ok: false,
29927
30188
  text: "",
29928
30189
  conversationId,
29929
30190
  error: "agy is not signed in — run /agy auth",
29930
30191
  durationMs: outcome.durationMs
29931
30192
  };
29932
- const joined = textParts.join("\n").trim();
29933
- const text = joined !== "" ? joined : resultText;
29934
30193
  if (outcome.code !== 0 && text === "") return {
29935
30194
  ok: false,
29936
30195
  text: "",
@@ -30417,7 +30676,11 @@ const HELP = [
30417
30676
  "**/agy** — Antigravity (agy CLI) bridge",
30418
30677
  "- `/agy status` — binary, version, auth, mode, catalog, bindings",
30419
30678
  "- `/agy pool` / `/agy accounts` — account pool, live quota %, cooldowns",
30679
+ "- `/agy goal [status|complete|clear|pause]` — inspect or control the current DSH goal",
30680
+ "- `/agy session [info|clear]` — view or drop the current session agy binding",
30420
30681
  "- `/agy add-account [alias]` — start OAuth login for a new account slot",
30682
+ "- `/agy enable-account <id>` — enable an account slot",
30683
+ "- `/agy disable-account <id>` — disable an account slot (skips in rotation)",
30421
30684
  "- `/agy refresh-quota` — query Google backend for fresh quota %",
30422
30685
  "- `/agy clear-cooldown` — reset cooldown timers across all accounts",
30423
30686
  "- `/agy remove-account <id>` — delete an account slot",
@@ -30434,12 +30697,12 @@ const HELP = [
30434
30697
  function agyCommandDefinition(deps) {
30435
30698
  return {
30436
30699
  name: "agy",
30437
- description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics",
30438
- input: { hint: "<status|pool|models|mode|effort|workspace|clear|doctor|help>" },
30439
- handler: (invocation) => handle(deps, invocation.rawInput)
30700
+ description: "Antigravity (agy CLI) bridge: status, login, models, mode, diagnostics, goal",
30701
+ input: { hint: "<status|pool|goal|session|enable-account|disable-account|models|mode|effort|workspace|clear|doctor|help>" },
30702
+ handler: (invocation) => handle(deps, invocation.rawInput, invocation)
30440
30703
  };
30441
30704
  }
30442
- async function handle(deps, raw) {
30705
+ async function handle(deps, raw, invocation) {
30443
30706
  const parts = raw.trim().split(/\s+/).filter(Boolean);
30444
30707
  const sub = parts[0] ?? "help";
30445
30708
  const arg = parts[1] ?? "";
@@ -30463,10 +30726,21 @@ async function handle(deps, raw) {
30463
30726
  const sec = Math.ceil((cd.cooldownUntil - Date.now()) / 1e3);
30464
30727
  cds.push(`${fam} cooldown: ${sec}s left (${cd.reason})`);
30465
30728
  }
30466
- lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, cds.length > 0 ? ` - ⚠️ ${cds.join("; ")}` : " - Status: 🟢 Ready");
30729
+ let statusText = " - Status: 🟢 Ready";
30730
+ if (!a.enabled) statusText = " - Status: ⏸️ Disabled";
30731
+ else if (a.authRequired) statusText = ` - Status: 🔴 Auth required (${a.authError || "sign in again"})`;
30732
+ else if (cds.length > 0) statusText = ` - ⚠️ ${cds.join("; ")}`;
30733
+ lines.push(`${i + 1}. **${a.alias}** (${a.email || a.id})${isPrimary ? " ⭐ primary" : ""}`, ` - Proxy: ${a.proxyUrl ? redactProxyUrl(a.proxyUrl) : deps.cfg().proxyUrl ? redactProxyUrl(deps.cfg().proxyUrl) : "system default"}`, ` - Quota: Gemini ${qG} | Claude ${qC} | GPT-OSS ${qO}`, statusText);
30467
30734
  }
30468
30735
  return ok(lines.join("\n"));
30469
30736
  }
30737
+ if (sub === "enable-account" || sub === "disable-account") {
30738
+ if (!arg) return err(`usage: /agy ${sub} <id>`);
30739
+ const pool = deps.pool?.();
30740
+ if (!pool) return err("Account pool not available");
30741
+ const enabled = sub === "enable-account";
30742
+ return pool.setAccountEnabled(arg, enabled) ? ok(`${enabled ? "Enabled" : "Disabled"} account ${arg}`) : err(`Account ${arg} not found`);
30743
+ }
30470
30744
  if (sub === "add-account") {
30471
30745
  const pool = deps.pool?.();
30472
30746
  if (!pool) return err("Account pool not available");
@@ -30498,6 +30772,164 @@ async function handle(deps, raw) {
30498
30772
  if (!pool) return err("Account pool not available");
30499
30773
  return pool.deleteAccount(arg) ? ok(`Removed account ${arg}`) : err(`Account ${arg} not found`);
30500
30774
  }
30775
+ if (sub === "goal") {
30776
+ const action = arg || "status";
30777
+ const session = invocation?.agent?.session ?? (deps.sessions?.() && invocation?.agent?.id ? deps.sessions().get(invocation.agent.id) : void 0);
30778
+ if (!session) return err("No active DSH session available for goal command.");
30779
+ const events = session.events;
30780
+ let latestGoalChange = null;
30781
+ if (events) for (let i = events.length - 1; i >= 0; i--) {
30782
+ const ev = events[i];
30783
+ if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
30784
+ const d = ev.data;
30785
+ if (d.kind === "goal/change") {
30786
+ latestGoalChange = d;
30787
+ break;
30788
+ }
30789
+ }
30790
+ }
30791
+ const activeGoal = latestGoalChange && latestGoalChange.operation !== "clear" ? latestGoalChange.goal : null;
30792
+ if (action === "status") {
30793
+ if (!activeGoal || !latestGoalChange) return ok("**DSH Goal Status**: No active goal in this session.");
30794
+ return ok([
30795
+ `**DSH Goal Status** (${{
30796
+ active: "🟢",
30797
+ paused: "⏸️",
30798
+ blocked: "🚫",
30799
+ complete: "✅"
30800
+ }[activeGoal.phase] ?? "⚪"} \`${activeGoal.phase}\`)`,
30801
+ `- **Goal ID**: \`${activeGoal.id}\` (rev ${activeGoal.revision})`,
30802
+ `- **Objective**: ${activeGoal.objective}`,
30803
+ `- **Rounds**: ${latestGoalChange.roundsStarted ?? 1} / ${activeGoal.maxGoalRounds ?? 256}`,
30804
+ ...activeGoal.blockedReason ? [`- **Blocked Reason**: ${activeGoal.blockedReason.message}`] : []
30805
+ ].join("\n"));
30806
+ }
30807
+ if (action === "complete") {
30808
+ if (!activeGoal || !latestGoalChange) return err("No active goal to complete.");
30809
+ if (typeof session.append === "function") {
30810
+ session.append("goal/change", {
30811
+ kind: "goal/change",
30812
+ version: 1,
30813
+ operation: "complete",
30814
+ goal: {
30815
+ id: activeGoal.id,
30816
+ revision: activeGoal.revision + 1,
30817
+ objective: activeGoal.objective,
30818
+ phase: "complete",
30819
+ maxGoalRounds: activeGoal.maxGoalRounds
30820
+ },
30821
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
30822
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
30823
+ updatedAt: Date.now()
30824
+ });
30825
+ return ok(`✅ Marked goal \`${activeGoal.id}\` as complete. Automatic continuation rounds stopped.`);
30826
+ }
30827
+ return err("Session cannot append events.");
30828
+ }
30829
+ if (action === "pause") {
30830
+ if (!activeGoal || !latestGoalChange) return err("No active goal to pause.");
30831
+ if (typeof session.append === "function") {
30832
+ session.append("goal/change", {
30833
+ kind: "goal/change",
30834
+ version: 1,
30835
+ operation: "pause",
30836
+ goal: {
30837
+ id: activeGoal.id,
30838
+ revision: activeGoal.revision + 1,
30839
+ objective: activeGoal.objective,
30840
+ phase: "paused",
30841
+ maxGoalRounds: activeGoal.maxGoalRounds
30842
+ },
30843
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
30844
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
30845
+ updatedAt: Date.now()
30846
+ });
30847
+ return ok(`⏸️ Paused goal \`${activeGoal.id}\`.`);
30848
+ }
30849
+ return err("Session cannot append events.");
30850
+ }
30851
+ if (action === "resume") {
30852
+ if (!activeGoal || !latestGoalChange) return err("No active goal to resume.");
30853
+ if (typeof session.append === "function") {
30854
+ session.append("goal/change", {
30855
+ kind: "goal/change",
30856
+ version: 1,
30857
+ operation: "resume",
30858
+ goal: {
30859
+ id: activeGoal.id,
30860
+ revision: activeGoal.revision + 1,
30861
+ objective: activeGoal.objective,
30862
+ phase: "active",
30863
+ maxGoalRounds: activeGoal.maxGoalRounds
30864
+ },
30865
+ roundsStarted: latestGoalChange.roundsStarted ?? 1,
30866
+ createdAt: latestGoalChange.createdAt ?? Date.now(),
30867
+ updatedAt: Date.now()
30868
+ });
30869
+ return ok(`▶️ Resumed goal \`${activeGoal.id}\`.`);
30870
+ }
30871
+ return err("Session cannot append events.");
30872
+ }
30873
+ if (action === "clear") {
30874
+ if (!activeGoal) return ok("No active goal to clear.");
30875
+ if (typeof session.append === "function") {
30876
+ session.append("goal/change", {
30877
+ kind: "goal/change",
30878
+ version: 1,
30879
+ operation: "clear",
30880
+ cleared: {
30881
+ id: activeGoal.id,
30882
+ revision: activeGoal.revision + 1
30883
+ },
30884
+ clearedAt: Date.now()
30885
+ });
30886
+ return ok(`Cleared goal \`${activeGoal.id}\`.`);
30887
+ }
30888
+ return err("Session cannot append events.");
30889
+ }
30890
+ return err(`Unknown goal action \`${action}\`. Usage: /agy goal [status|complete|pause|resume|clear]`);
30891
+ }
30892
+ if (sub === "session") {
30893
+ const action = arg || "info";
30894
+ const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
30895
+ const session = invocation?.agent?.session ?? (deps.sessions?.() && sessionId ? deps.sessions().get(sessionId) : void 0);
30896
+ if (action === "info") {
30897
+ const lines = ["**DSH Session & agy Bridge Info**"];
30898
+ if (sessionId) {
30899
+ lines.push(`- **Session ID**: \`${sessionId}\``);
30900
+ const cwd = session?.header?.cwd;
30901
+ if (cwd) lines.push(`- **Workspace CWD**: \`${cwd}\``);
30902
+ const bindings = deps.store().all();
30903
+ const matchedKey = Object.keys(bindings).find((k) => k === sessionId || k.startsWith(`${sessionId}:`));
30904
+ if (matchedKey && bindings[matchedKey]) {
30905
+ const b = bindings[matchedKey];
30906
+ lines.push(`- **Bound agy Conversation**: \`${b.conversationId}\``, `- **Bound Model**: \`${b.model || "default"}\``, `- **Last Message Count**: ${b.lastMessageCount}`, `- **Last Updated**: ${new Date(b.updatedAt).toLocaleTimeString()}`);
30907
+ } else lines.push("- **Bound agy Conversation**: *(none - next turn starts fresh)*");
30908
+ if (session?.events) lines.push(`- **Total Session Events**: ${session.events.length}`);
30909
+ } else lines.push("- Current session ID could not be identified from invocation context.");
30910
+ return ok(lines.join("\n"));
30911
+ }
30912
+ if (action === "clear") {
30913
+ if (!sessionId) {
30914
+ const all = deps.store().all();
30915
+ const keys = Object.keys(all);
30916
+ if (keys.length === 0) return ok("No conversation bindings yet.");
30917
+ const key = keys.reduce((a, b) => (all[a]?.updatedAt ?? 0) >= (all[b]?.updatedAt ?? 0) ? a : b);
30918
+ const dropped = all[key];
30919
+ if (dropped === void 0) return ok("No conversation bindings yet.");
30920
+ deps.store().delete(key);
30921
+ return ok(`Dropped binding for session \`${key}\` (agy conversation \`${dropped.conversationId}\`). Next turn starts fresh.`);
30922
+ }
30923
+ const bindings = deps.store().all();
30924
+ let count = 0;
30925
+ for (const k of Object.keys(bindings)) if (k === sessionId || k.startsWith(`${sessionId}:`)) {
30926
+ deps.store().delete(k);
30927
+ count++;
30928
+ }
30929
+ return ok(`Dropped ${count > 0 ? count : "any"} agy conversation binding(s) for session \`${sessionId}\`. Next turn will start a fresh agy conversation.`);
30930
+ }
30931
+ return err(`Unknown session action \`${action}\`. Usage: /agy session [info|clear]`);
30932
+ }
30501
30933
  if (sub === "auth") {
30502
30934
  const flow = deps.poolAuth?.();
30503
30935
  if (!flow) return err("auth flow not available");
@@ -30558,6 +30990,15 @@ async function handle(deps, raw) {
30558
30990
  return ok("Workspace set to **" + arg + "** — effective next turn.");
30559
30991
  }
30560
30992
  if (sub === "clear") {
30993
+ const sessionId = invocation?.agent?.id ? String(invocation.agent.id) : void 0;
30994
+ if (sessionId) {
30995
+ const bindings = deps.store().all();
30996
+ const keys = Object.keys(bindings).filter((k) => k === sessionId || k.startsWith(`${sessionId}:`));
30997
+ if (keys.length > 0) {
30998
+ for (const k of keys) deps.store().delete(k);
30999
+ return ok(`Dropped binding for session \`${sessionId}\`. The next turn starts a fresh agy conversation.`);
31000
+ }
31001
+ }
30561
31002
  const all = deps.store().all();
30562
31003
  const keys = Object.keys(all);
30563
31004
  if (keys.length === 0) return ok("No conversation bindings yet.");
@@ -31941,6 +32382,22 @@ function apply(ctx, entryConfig = {}) {
31941
32382
  sessionCwd: (sessionId) => {
31942
32383
  return ctx.get("sessions")?.get(sessionId)?.header?.cwd;
31943
32384
  },
32385
+ sessionGoal: (sessionId) => {
32386
+ const session = (ctx.get("sessions") ?? ctx.sessions)?.get(sessionId);
32387
+ if (!session?.events) return null;
32388
+ for (let i = session.events.length - 1; i >= 0; i--) {
32389
+ const ev = session.events[i];
32390
+ if (ev?.type === "goal/change" && ev.data && typeof ev.data === "object") {
32391
+ const d = ev.data;
32392
+ if (d.operation === "clear") return null;
32393
+ if (d.goal?.id) return {
32394
+ goalId: d.goal.id,
32395
+ revision: typeof d.goal.revision === "number" ? d.goal.revision : 1
32396
+ };
32397
+ }
32398
+ }
32399
+ return null;
32400
+ },
31944
32401
  syncSessionModel: (sessionId, model, effort) => {
31945
32402
  if (!sessionId) return;
31946
32403
  try {
@@ -32046,6 +32503,7 @@ function apply(ctx, entryConfig = {}) {
32046
32503
  quota: () => quota,
32047
32504
  lastRun: () => lastRun,
32048
32505
  setOverride,
32506
+ sessions: () => ctx.get("sessions") ?? ctx.sessions,
32049
32507
  runDoctor: async () => {
32050
32508
  return writeDoctorReport({
32051
32509
  cfg: getConfig,
@@ -32415,6 +32873,26 @@ function apply(ctx, entryConfig = {}) {
32415
32873
  })();
32416
32874
  }
32417
32875
  });
32876
+ reg({
32877
+ kind: "exact",
32878
+ path: "/plugins/agy-link/pool/enable",
32879
+ handler: (req, res) => {
32880
+ (async () => {
32881
+ if (methodOf(req) !== "POST") {
32882
+ sendJson(res, 405, { error: "POST only" });
32883
+ return;
32884
+ }
32885
+ const body = await readBody(req);
32886
+ const id = typeof body.id === "string" ? body.id : "";
32887
+ const enabled = typeof body.enabled === "boolean" ? body.enabled : true;
32888
+ const ok = pool.setAccountEnabled(id, enabled);
32889
+ sendJson(res, ok ? 200 : 400, {
32890
+ ok,
32891
+ pool: pool.getPoolData()
32892
+ });
32893
+ })();
32894
+ }
32895
+ });
32418
32896
  reg({
32419
32897
  kind: "exact",
32420
32898
  path: "/plugins/agy-link/pool/proxy",
@@ -32515,6 +32993,25 @@ function apply(ctx, entryConfig = {}) {
32515
32993
  })();
32516
32994
  }
32517
32995
  });
32996
+ reg({
32997
+ kind: "exact",
32998
+ path: "/plugins/agy-link/pool/set-active",
32999
+ handler: (req, res) => {
33000
+ (async () => {
33001
+ if (methodOf(req) !== "POST") {
33002
+ sendJson(res, 405, { error: "POST only" });
33003
+ return;
33004
+ }
33005
+ const body = await readBody(req);
33006
+ const id = typeof body.id === "string" ? body.id : "";
33007
+ const ok = id !== "" && pool.setActiveAccount(id);
33008
+ sendJson(res, ok ? 200 : 400, {
33009
+ ok,
33010
+ pool: pool.getPoolData()
33011
+ });
33012
+ })();
33013
+ }
33014
+ });
32518
33015
  reg({
32519
33016
  kind: "exact",
32520
33017
  path: "/plugins/agy-link/pool/reorder",