@ponythewhite/base-context 1.0.14 → 1.0.16

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/base-context-runtime/pyproject.toml +1 -1
  3. package/dist/base-context-runtime/src/rlm/bash.py +19 -0
  4. package/dist/base-context-runtime/src/rlm/job_watch.py +156 -0
  5. package/dist/base-context-runtime/src/rlm/repl.md +28 -6
  6. package/dist/base-context-runtime/src/rlm/repl.py +6 -1
  7. package/dist/base-context-runtime/test/test_bash.py +4 -4
  8. package/dist/base-context-runtime/test/test_job_watch.py +67 -0
  9. package/dist/base-context-runtime/uv.lock +1 -1
  10. package/dist/build-info.json +1 -1
  11. package/dist/bundle/{bundled-modules-UODO7ZR7.js → bundled-modules-2DMTK7VK.js} +2 -2
  12. package/dist/bundle/{chunk-SX7CAXWU.js → chunk-MSH6QRP5.js} +1730 -867
  13. package/dist/bundle/{chunk-NLRWERWA.js → chunk-T2OK7SRV.js} +53 -21
  14. package/dist/bundle/{cli-main-6GXXIMQQ.js → cli-main-WL2L5IEQ.js} +2 -2
  15. package/dist/bundle/cli.js +1 -1
  16. package/dist/bundle/{main-HF64B4HM.js → main-XPIEIEQJ.js} +2 -2
  17. package/dist/core/agent-session.d.ts +2 -0
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +191 -33
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction/compaction.d.ts +1 -1
  22. package/dist/core/compaction/compaction.d.ts.map +1 -1
  23. package/dist/core/compaction/compaction.js +11 -9
  24. package/dist/core/compaction/compaction.js.map +1 -1
  25. package/dist/core/history-index-worker.d.ts.map +1 -1
  26. package/dist/core/history-index-worker.js +22 -1
  27. package/dist/core/history-index-worker.js.map +1 -1
  28. package/dist/core/history-index.d.ts +2 -0
  29. package/dist/core/history-index.d.ts.map +1 -1
  30. package/dist/core/history-index.js.map +1 -1
  31. package/dist/core/inference-coordinator.d.ts.map +1 -1
  32. package/dist/core/inference-coordinator.js +13 -2
  33. package/dist/core/inference-coordinator.js.map +1 -1
  34. package/dist/core/job-watch.d.ts +120 -0
  35. package/dist/core/job-watch.d.ts.map +1 -0
  36. package/dist/core/job-watch.js +648 -0
  37. package/dist/core/job-watch.js.map +1 -0
  38. package/dist/core/kernel/repl-manager.d.ts +5 -1
  39. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  40. package/dist/core/kernel/repl-manager.js +57 -0
  41. package/dist/core/kernel/repl-manager.js.map +1 -1
  42. package/dist/core/kernel/shared.d.ts +12 -0
  43. package/dist/core/kernel/shared.d.ts.map +1 -1
  44. package/dist/core/kernel/shared.js.map +1 -1
  45. package/dist/core/prompts/rlm.d.ts.map +1 -1
  46. package/dist/core/prompts/rlm.js +49 -39
  47. package/dist/core/prompts/rlm.js.map +1 -1
  48. package/dist/core/session-manager.d.ts +10 -0
  49. package/dist/core/session-manager.d.ts.map +1 -1
  50. package/dist/core/session-manager.js +66 -1
  51. package/dist/core/session-manager.js.map +1 -1
  52. package/dist/core/tools/prime-context.d.ts.map +1 -1
  53. package/dist/core/tools/prime-context.js +1 -1
  54. package/dist/core/tools/prime-context.js.map +1 -1
  55. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  56. package/dist/modes/daemon/daemon-mode.js +17 -17
  57. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  58. package/dist/modes/daemon/daemon-worker-client.d.ts +1 -0
  59. package/dist/modes/daemon/daemon-worker-client.d.ts.map +1 -1
  60. package/dist/modes/daemon/daemon-worker-client.js +8 -0
  61. package/dist/modes/daemon/daemon-worker-client.js.map +1 -1
  62. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  63. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  64. package/dist/modes/interactive/interactive-mode.js +31 -5
  65. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  66. package/dist/skills/agent-message/SKILL.md +8 -2
  67. package/dist/skills/bounded-inspect/SKILL.md +78 -0
  68. package/dist/skills/bounded-inspect/pyproject.toml +16 -0
  69. package/dist/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
  70. package/dist/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
  71. package/dist/skills/job-watch/SKILL.md +43 -0
  72. package/dist/skills/job-watch/pyproject.toml +13 -0
  73. package/dist/skills/job-watch/src/job_watch/__init__.py +61 -0
  74. package/dist/skills/rlm-heartbeat/SKILL.md +10 -6
  75. package/dist/utils/file-lines.d.ts.map +1 -1
  76. package/dist/utils/file-lines.js +6 -2
  77. package/dist/utils/file-lines.js.map +1 -1
  78. package/docs/rlm-runtime.md +4 -1
  79. package/docs/skills.md +18 -26
  80. package/docs/upstream-1.0.15.md +83 -0
  81. package/package.json +4 -4
  82. package/skills/agent-message/SKILL.md +8 -2
  83. package/skills/bounded-inspect/SKILL.md +78 -0
  84. package/skills/bounded-inspect/pyproject.toml +16 -0
  85. package/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
  86. package/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
  87. package/skills/job-watch/SKILL.md +43 -0
  88. package/skills/job-watch/pyproject.toml +13 -0
  89. package/skills/job-watch/src/job_watch/__init__.py +61 -0
  90. package/skills/rlm-heartbeat/SKILL.md +10 -6
@@ -27,6 +27,7 @@ import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
27
27
  import { emitSessionShutdownEvent } from "./extensions/runner.js";
28
28
  import { createGoalContextMessage, emptyGoalState, GOAL_CONTEXT_CUSTOM_TYPE, GOAL_CONTEXT_PREVIEW_LABEL, GOAL_SKILL_NAME, GOAL_STATE_CUSTOM_TYPE, goalHostResponse, goalTokenDeltaForUsage, isPersistedGoalState, normalizeGoalState, validateGoalBudget, validateGoalObjective, } from "./goals.js";
29
29
  import { captureNativeBranchRequests, captureNativeCompactionRequests, captureNativePlannerRequests, captureNativeReviewerRequests, InferenceCoordinator, takeNativeInferenceAuthSource, } from "./inference-coordinator.js";
30
+ import { JOB_WATCH_STATE, JobWatchController } from "./job-watch.js";
30
31
  import { snapshotPathIn } from "./kernel/state-snapshot.js";
31
32
  import { convertToLlm, createCompactionOutcomeMessage, createHeartbeatPromptMessage, createRefinementOutcomeMessage, createRlmChildFailureMessage, createRlmChildTerminalNoticeMessage, createSessionSlashCommandMessage, createSessionSlashCommandResultMessage, HARNESS_SNAPSHOT_CUSTOM_TYPE, HEARTBEAT_PROMPT_CUSTOM_TYPE, HEARTBEAT_PROMPT_PREVIEW_LABEL, IPYTHON_STATE_RESTORED_CUSTOM_TYPE, isSessionSlashCommandMessage, RLM_CHILD_FAILURE_CUSTOM_TYPE, RLM_CHILD_TERMINAL_NOTICE_CUSTOM_TYPE, } from "./messages.js";
32
33
  import { throwIfPromptAdmissionCancelled } from "./prompt-admission.js";
@@ -35,6 +36,7 @@ import { appendGlobalRefinement, applyRefinementProposal, formatHarnessStateForP
35
36
  import { getRecoveryCompactionAuthorization, PublicContextBudgetError } from "./request-view-selection.js";
36
37
  import { resolveConfigValue } from "./resolve-config-value.js";
37
38
  import { assertResourceCurrent } from "./resource-view.js";
39
+ import { retainToolOutput } from "./retained-tool-output.js";
38
40
  import { createDefaultRlmSubagentSessionName, createRlmDeleteSubagentHostHandler, createRlmFindModelsHostHandler, createRlmListSubagentsHostHandler, createRlmRunHostHandler, findRlmModelMatches, normalizeRequestedRlmSubagentModel, normalizeRequestedRlmSubagentSessionName, normalizeRequestedRlmSubagentThinkingLevel, } from "./rlm-runtime.js";
39
41
  import { captureSelectedSkill, captureSkillDescriptor, readSkillSelection, sameSelectedSkills, selectedSkillBlock, selectedSkillIdentity, } from "./selected-skills.js";
40
42
  import { createNativeRecoveryRefusal, DEFAULT_NATIVE_RECOVERY_LIMITS, NativeRecoveryBudgetRefusal, NativeRecoveryCursorStore, parseNativeRecoveryInput, recoverCapturedHistory, } from "./selective-recovery.js";
@@ -328,6 +330,7 @@ export class AgentSession {
328
330
  _goalStateRevision = 0;
329
331
  _goalAccountingStartedAt = undefined;
330
332
  _goalContinuationAwaitsRlmWork = false;
333
+ _jobWatchController;
331
334
  _goalAccountedAssistantMessages = new WeakSet();
332
335
  _goalAbortInProgress = false;
333
336
  _autonomousState;
@@ -1110,6 +1113,10 @@ export class AgentSession {
1110
1113
  activeToolNames: this._initialActiveToolNames,
1111
1114
  includeAllExtensionTools: true,
1112
1115
  });
1116
+ if (bootstrap.jobWatchState)
1117
+ await this._getJobWatchController().restore(bootstrap.jobWatchState);
1118
+ if (bootstrap.jobWatchUnavailable)
1119
+ this._surfaceSessionInputError(new Error(bootstrap.jobWatchUnavailable));
1113
1120
  if (this._goalState.status === "active" &&
1114
1121
  this._includeGoals &&
1115
1122
  !this.getActiveToolNames().includes("ipython")) {
@@ -1625,8 +1632,28 @@ export class AgentSession {
1625
1632
  !isPersistedRlmMaxDepthState(hydrated.entry.data))
1626
1633
  throw new Error("Bootstrap RLM depth source is unavailable or ineligible");
1627
1634
  rlmMaxDepth = hydrated.entry.data;
1635
+ remaining -= bootstrap.rlmMaxDepth.locator.length;
1636
+ }
1637
+ let jobWatchState;
1638
+ let jobWatchUnavailable = bootstrap.jobWatchUnavailable
1639
+ ? `Monitoring unavailable: optional job-watch bootstrap candidate budget exceeded. Inspect ${this.sessionManager.getSessionFile()}; no watches were resumed.`
1640
+ : undefined;
1641
+ if (bootstrap.jobWatchState) {
1642
+ if (bootstrap.jobWatchState.locator.length >
1643
+ Math.min(remaining, 64 * 1024 + 1024, Math.floor(maxSourceBytes / 2) + 1024)) {
1644
+ jobWatchUnavailable = `Monitoring unavailable: optional job-watch state exceeds the existing source budget. Inspect ${this.sessionManager.getSessionFile()} entry ${bootstrap.jobWatchState.id}; no watches were resumed.`;
1645
+ }
1646
+ else {
1647
+ const hydrated = await view.hydrateEntry(bootstrap.jobWatchState.id, remaining);
1648
+ if (hydrated?.entry.type === "custom" &&
1649
+ hydrated.entry.customType === JOB_WATCH_STATE &&
1650
+ hydrated.source.retention !== "retained-import")
1651
+ jobWatchState = hydrated.entry.data;
1652
+ }
1628
1653
  }
1629
1654
  return {
1655
+ jobWatchState,
1656
+ jobWatchUnavailable,
1630
1657
  goalState,
1631
1658
  goalSeedable: bootstrap.goalSeedable,
1632
1659
  rlmMaxDepth,
@@ -1675,6 +1702,8 @@ export class AgentSession {
1675
1702
  return true;
1676
1703
  }
1677
1704
  async _reloadBranchRuntimeState() {
1705
+ this._jobWatchController?.dispose();
1706
+ this._jobWatchController = undefined;
1678
1707
  this._contextMode = await this._readContextMode();
1679
1708
  const bootstrap = this.sessionManager.isPersisted()
1680
1709
  ? await this._readRuntimeBootstrap()
@@ -2123,7 +2152,10 @@ export class AgentSession {
2123
2152
  async _resumeGoalContinuationAfterRlmWork() {
2124
2153
  if (!this._goalContinuationAwaitsRlmWork)
2125
2154
  return;
2126
- if (this._disposed || this._disposing || this._hasUnsettledRlmQuiescenceWork())
2155
+ if (this._disposed ||
2156
+ this._disposing ||
2157
+ this._hasUnsettledRlmQuiescenceWork() ||
2158
+ this._jobWatchController?.isParked())
2127
2159
  return;
2128
2160
  if (this._goalState.status !== "active" || !this._goalState.objective) {
2129
2161
  this._goalContinuationAwaitsRlmWork = false;
@@ -3002,7 +3034,9 @@ export class AgentSession {
3002
3034
  : undefined;
3003
3035
  }
3004
3036
  async _queueAutonomousContinuationForThresholdCompaction(message, owner = this._captureCompactionOwner(this.agent.signal), capture = this._captureThresholdAutonomousOwner()) {
3005
- if (!this._isCompactionOwnerCurrent(owner) || !this._contextOptimizationAllowed())
3037
+ if (!this._isCompactionOwnerCurrent(owner) ||
3038
+ !this._contextOptimizationAllowed() ||
3039
+ this._jobWatchController?.isParked())
3006
3040
  return undefined;
3007
3041
  const queued = this._queuedAutonomousThresholdContinuations.get(message);
3008
3042
  if (queued && this._checkpointActionPending(queued))
@@ -3059,7 +3093,7 @@ export class AgentSession {
3059
3093
  return undefined;
3060
3094
  if (message.stopReason === "error" || message.stopReason === "aborted")
3061
3095
  return undefined;
3062
- if (owner.goal.status !== "active" || !owner.goal.objective)
3096
+ if (owner.goal.status !== "active" || !owner.goal.objective || this._jobWatchController?.isParked())
3063
3097
  return undefined;
3064
3098
  const queued = this._queuedGoalThresholdContinuation;
3065
3099
  if (queued && this._checkpointActionPending(queued))
@@ -3217,14 +3251,14 @@ export class AgentSession {
3217
3251
  this._assertCompactionOwner(owner);
3218
3252
  const compaction = owner.manager.bindCompactionSink();
3219
3253
  const captured = owner.requests.capture(compaction);
3220
- let branch;
3221
3254
  let preparation;
3255
+ let leafKind;
3222
3256
  try {
3223
- branch = await compaction.readBranch();
3224
- this._assertCompactionOwner(owner);
3225
- preparation = this.isStreaming
3226
- ? (await this._prepareCapturedCompaction(branch, settings, captured, compaction, owner)).preparation
3227
- : undefined;
3257
+ if (this.isStreaming) {
3258
+ const prepared = await this._prepareCapturedCompaction(settings, captured, compaction, owner);
3259
+ preparation = prepared.preparation;
3260
+ leafKind = prepared.leafKind;
3261
+ }
3228
3262
  }
3229
3263
  catch (error) {
3230
3264
  try {
@@ -3245,10 +3279,9 @@ export class AgentSession {
3245
3279
  reason: "no active turn; compaction can only be requested while a turn is running",
3246
3280
  };
3247
3281
  if (!preparation) {
3248
- const lastEntry = branch.at(-1);
3249
3282
  return {
3250
3283
  scheduled: false,
3251
- reason: lastEntry?.type === "compaction" ? "already compacted" : "session is too short to compact",
3284
+ reason: leafKind === "compaction" ? "already compacted" : "session is too short to compact",
3252
3285
  };
3253
3286
  }
3254
3287
  this._assertContextOptimizationAllowed();
@@ -3549,6 +3582,8 @@ export class AgentSession {
3549
3582
  if (this._goalState.status !== "active" || !this._goalState.objective) {
3550
3583
  return { kind: "finish" };
3551
3584
  }
3585
+ if (this._jobWatchController?.isParked())
3586
+ return { kind: "wait_for_owned_work" };
3552
3587
  // Delegating and ending the turn is correct behavior; retain the existing wakeup owner.
3553
3588
  if (this._hasUnsettledRlmQuiescenceWork()) {
3554
3589
  this._goalContinuationAwaitsRlmWork = true;
@@ -3591,6 +3626,8 @@ export class AgentSession {
3591
3626
  return this._stoppedContinuationOutcome(signal);
3592
3627
  if (this.queuedActionCount > 0)
3593
3628
  return { kind: "finish" };
3629
+ if (this._jobWatchController?.isParked())
3630
+ return { kind: "wait_for_owned_work" };
3594
3631
  const owner = this._captureGoalContinuationOwner(signal);
3595
3632
  const arrivalEpoch = this._sessionInputArrivalEpoch;
3596
3633
  const goalSnapshot = owner.goal;
@@ -4413,6 +4450,13 @@ export class AgentSession {
4413
4450
  await drain(() => this._agentEventQueue);
4414
4451
  await drain(() => this._goalResumeOperation);
4415
4452
  await drain(() => this._waitForChildUsageWrites());
4453
+ if (this._jobWatchController) {
4454
+ const undelivered = new Set(this._actionStore
4455
+ .unfinishedActions()
4456
+ .filter((action) => action.queueKey && action.payload.kind === "turn" && !primaryDeliveryRecord(action).durable)
4457
+ .map((action) => action.queueKey));
4458
+ await drain(() => this._jobWatchController?.settleShutdown(undelivered));
4459
+ }
4416
4460
  await drain(() => this.dispose());
4417
4461
  }
4418
4462
  catch (error) {
@@ -4461,6 +4505,7 @@ export class AgentSession {
4461
4505
  }
4462
4506
  this.requests.stopAdmission();
4463
4507
  this._disposed = true;
4508
+ this._jobWatchController?.dispose();
4464
4509
  for (const run of this._unsettledRlmChildRuns)
4465
4510
  run.suppressTerminalNotice = true;
4466
4511
  for (const controller of this._rlmQuiescenceWaitAborts)
@@ -4813,10 +4858,10 @@ export class AgentSession {
4813
4858
  /** Persist mutable advice before the request owner captures its source and epoch ACK boundary. */
4814
4859
  async _appendHarnessSnapshotIfChanged() {
4815
4860
  const manager = this.sessionManager;
4816
- let leafId = manager.getLeafId();
4817
- const sourceIsCurrent = manager.captureCompactionSourceOwner();
4861
+ const sourceIsCurrent = manager.captureCompactionContentOwner();
4862
+ let ownAppends = 0;
4818
4863
  const assertCurrent = () => {
4819
- if (this._disposed || this.sessionManager !== manager || !sourceIsCurrent() || manager.getLeafId() !== leafId)
4864
+ if (this._disposed || this.sessionManager !== manager || !sourceIsCurrent(ownAppends))
4820
4865
  throw new Error("Harness snapshot source changed before request ownership");
4821
4866
  };
4822
4867
  let previous;
@@ -4845,8 +4890,9 @@ export class AgentSession {
4845
4890
  });
4846
4891
  if (previous === content)
4847
4892
  return assertCurrent;
4848
- const entryId = await manager.appendCustomMessageEntryWithRollback(HARNESS_SNAPSHOT_CUSTOM_TYPE, content, false);
4849
- if (this._disposed || this.sessionManager !== manager || !sourceIsCurrent() || manager.getLeafId() !== entryId)
4893
+ await manager.appendCustomMessageEntryWithRollback(HARNESS_SNAPSHOT_CUSTOM_TYPE, content, false);
4894
+ ownAppends++;
4895
+ if (this._disposed || this.sessionManager !== manager || !sourceIsCurrent(ownAppends))
4850
4896
  throw new Error("Harness snapshot source changed during append");
4851
4897
  if (!manager.isPersisted())
4852
4898
  this.agent.state.messages.push({
@@ -4856,7 +4902,6 @@ export class AgentSession {
4856
4902
  display: false,
4857
4903
  timestamp: Date.now(),
4858
4904
  });
4859
- leafId = entryId;
4860
4905
  return assertCurrent;
4861
4906
  }
4862
4907
  _refreshExtensionSystemPrompt(extensionPrompt, baseSnapshot) {
@@ -6492,10 +6537,13 @@ export class AgentSession {
6492
6537
  actionId: action.id,
6493
6538
  submittedText: input.submitted?.text,
6494
6539
  };
6540
+ const clearedGoal = Boolean(this._goalState.objective) && this._parseGoalSlashCommand(input.text)?.kind === "clear";
6495
6541
  await this._handleGoalSlashCommand(input.text, input.images, goalOrigin);
6496
6542
  resultText = this._goalState.objective
6497
6543
  ? `Goal ${this._goalState.status}: ${this._goalState.objective}`
6498
- : "No active goal.";
6544
+ : clearedGoal
6545
+ ? "Goal cleared."
6546
+ : "No active goal.";
6499
6547
  break;
6500
6548
  }
6501
6549
  case "autonomous":
@@ -7063,6 +7111,7 @@ export class AgentSession {
7063
7111
  this._advanceCheckpointPauseEpoch();
7064
7112
  this._notifySessionInputCheckpointChange();
7065
7113
  this._flushDeferredRlmTerminalNotices();
7114
+ void this._jobWatchController?.flushPending().catch((error) => this._surfaceSessionInputError(error));
7066
7115
  this._scheduleGoalContinuationAfterRlmWork();
7067
7116
  this._scheduleSessionInputPump();
7068
7117
  },
@@ -7179,6 +7228,7 @@ export class AgentSession {
7179
7228
  this._sessionInputPumpEpoch++;
7180
7229
  this._notifySessionInputCheckpointChange();
7181
7230
  this._flushDeferredRlmTerminalNotices();
7231
+ void this._jobWatchController?.flushPending().catch((error) => this._surfaceSessionInputError(error));
7182
7232
  }
7183
7233
  /** Resume the scheduler after requestAbort/abortForUpdateRestart suspended it; owned pause leases are unaffected. */
7184
7234
  resumeQueuedWork() {
@@ -7786,14 +7836,12 @@ export class AgentSession {
7786
7836
  const semanticEdges = owner.semanticEdges;
7787
7837
  compaction = owner.manager.bindCompactionSink();
7788
7838
  requests = owner.requests.capture(compaction);
7789
- const pathEntries = await compaction.readBranch();
7790
7839
  this._assertCompactionOwner(owner);
7791
7840
  const { apiKey, headers } = await this._getRequiredRequestAuth(model);
7792
7841
  this._assertCompactionOwner(owner);
7793
7842
  committed = await this._performCompaction({
7794
7843
  model,
7795
7844
  thinkingLevel,
7796
- pathEntries,
7797
7845
  settings,
7798
7846
  requests,
7799
7847
  compaction,
@@ -7887,33 +7935,47 @@ export class AgentSession {
7887
7935
  }
7888
7936
  }
7889
7937
  /** A private compiled context keeps extension summary edits separate from retained source recipes. */
7890
- async _prepareCapturedCompaction(pathEntries, settings, requests, compaction, owner = this._captureCompactionOwner(), allowShortSession = false, budgetPressure = false, capacity) {
7938
+ async _prepareCapturedCompaction(settings, requests, compaction, owner = this._captureCompactionOwner(), allowShortSession = false, budgetPressure = false, capacity) {
7891
7939
  this._assertCompactionOwner(owner);
7892
7940
  const limits = this.settingsManager.getCanonicalContextLimits();
7893
7941
  const resource = this._captureKernelResource();
7894
7942
  const source = await compaction.source;
7895
7943
  this._assertCompactionOwner(owner);
7896
- if (!source.persistent)
7944
+ if (!source.persistent) {
7945
+ const pathEntries = await compaction.readBranch();
7946
+ this._assertCompactionOwner(owner);
7897
7947
  return {
7898
7948
  preparation: prepareCompaction(pathEntries, settings, allowShortSession),
7949
+ pathEntries,
7950
+ leafKind: pathEntries.at(-1)?.type,
7899
7951
  maxSourceBytes: limits.maxSourceBytes,
7900
7952
  };
7953
+ }
7901
7954
  return requests.readHistory(async (view) => {
7902
7955
  const messages = await new CanonicalContextCompiler().compile(view, limits, undefined, {}, resource, this._initialContextMode, false, "read");
7903
7956
  this._assertCompactionOwner(owner);
7904
7957
  const context = getCanonicalEpochContext(messages);
7905
- if (context.checkpoint?.includeSummary && pathEntries.at(-1)?.type === "compaction")
7906
- return { preparation: undefined, maxSourceBytes: limits.maxSourceBytes };
7958
+ const entryIds = context.references.map((ref) => ref?.ref.entryId);
7959
+ const selected = await compaction.readCompactionEntries(entryIds.filter((id) => id !== undefined));
7960
+ this._assertCompactionOwner(owner);
7961
+ const pathEntries = selected.entries;
7962
+ if (context.checkpoint?.includeSummary && selected.leafKind === "compaction")
7963
+ return {
7964
+ preparation: undefined,
7965
+ pathEntries,
7966
+ leafKind: selected.leafKind,
7967
+ maxSourceBytes: limits.maxSourceBytes,
7968
+ };
7907
7969
  const authorization = getRecoveryCompactionAuthorization(capacity);
7908
7970
  const boundary = canonicalRecoveryBoundary(messages, authorization);
7909
- const preparation = budgetPressure || context.checkpoint || boundary
7910
- ? prepareViewCompaction(messages, context.references.map((ref) => ref?.ref.entryId), pathEntries, settings, boundary, allowShortSession, budgetPressure)
7911
- : prepareCompaction(pathEntries, settings, allowShortSession);
7971
+ const preparation = prepareViewCompaction(messages, entryIds, pathEntries, settings, boundary, allowShortSession, budgetPressure, selected.suffixAnchors);
7912
7972
  // Refuse unsupported public data or open groups before starting the summary model call.
7913
7973
  if (preparation)
7914
7974
  prepareRecoveryCompaction(messages, preparation.firstKeptEntryId, limits.maxSourceBytes, authorization);
7915
7975
  return {
7916
7976
  preparation,
7977
+ pathEntries,
7978
+ leafKind: selected.leafKind,
7917
7979
  messages,
7918
7980
  resource: context.resourceRevision !== undefined ? resource : undefined,
7919
7981
  maxSourceBytes: limits.maxSourceBytes,
@@ -7926,15 +7988,15 @@ export class AgentSession {
7926
7988
  * Error("Compaction cancelled") on abort or extension cancel.
7927
7989
  */
7928
7990
  async _performCompaction(options) {
7929
- const { model, apiKey, headers, customInstructions, signal, thinkingLevel, requests, compaction, semanticEdges, pathEntries, settings, owner = this._captureCompactionOwner(), allowShortSession = false, budgetPressure = false, } = options;
7930
- const prepared = await this._prepareCapturedCompaction(pathEntries, settings, requests, compaction, owner, allowShortSession, budgetPressure, options.capacity);
7991
+ const { model, apiKey, headers, customInstructions, signal, thinkingLevel, requests, compaction, semanticEdges, settings, owner = this._captureCompactionOwner(), allowShortSession = false, budgetPressure = false, } = options;
7992
+ const prepared = await this._prepareCapturedCompaction(settings, requests, compaction, owner, allowShortSession, budgetPressure, options.capacity);
7931
7993
  this._assertCompactionOwner(owner);
7932
7994
  if (prepared.resource)
7933
7995
  assertResourceCurrent(prepared.resource);
7934
7996
  const preparation = prepared.preparation;
7997
+ const pathEntries = prepared.pathEntries;
7935
7998
  if (!preparation) {
7936
- const lastEntry = pathEntries[pathEntries.length - 1];
7937
- if (lastEntry?.type === "compaction") {
7999
+ if (prepared.leafKind === "compaction") {
7938
8000
  throw new CompactionSkippedError("Already compacted");
7939
8001
  }
7940
8002
  throw new CompactionSkippedError("Session is too short to compact — try again once it grows");
@@ -9366,7 +9428,6 @@ export class AgentSession {
9366
9428
  const semanticEdges = owner.semanticEdges;
9367
9429
  compaction = owner.manager.bindCompactionSink();
9368
9430
  requests = owner.requests.capture(compaction);
9369
- const pathEntries = await compaction.readBranch();
9370
9431
  this._assertCompactionOwner(owner);
9371
9432
  const authResult = model ? await this._modelRegistry.getApiKeyAndHeaders(model) : undefined;
9372
9433
  this._assertCompactionOwner(owner);
@@ -9385,7 +9446,6 @@ export class AgentSession {
9385
9446
  committed = await this._performCompaction({
9386
9447
  model,
9387
9448
  thinkingLevel,
9388
- pathEntries,
9389
9449
  settings,
9390
9450
  requests,
9391
9451
  compaction,
@@ -10015,6 +10075,75 @@ export class AgentSession {
10015
10075
  return createNativeRecoveryRefusal("unavailable", "captured_history_unavailable", responseBytes);
10016
10076
  }
10017
10077
  }
10078
+ _getJobWatchController() {
10079
+ if (this._jobWatchController?.isCurrent())
10080
+ return this._jobWatchController;
10081
+ this._jobWatchController?.dispose();
10082
+ const manager = this.sessionManager;
10083
+ const ownsSource = manager.captureCompactionSourceOwner();
10084
+ const controller = new JobWatchController({
10085
+ sessionId: manager.getSessionId(),
10086
+ sessionFile: manager.getSessionFile(),
10087
+ current: () => !this._disposed && !this._disposing && this.sessionManager === manager && ownsSource(),
10088
+ goalId: () => this._goalState.goalId,
10089
+ persist: async (state) => {
10090
+ if (this.sessionManager !== manager || !ownsSource())
10091
+ return;
10092
+ await manager.appendCustomEntry(JOB_WATCH_STATE, state);
10093
+ },
10094
+ waitForDelivery: true,
10095
+ stateBudgetBytes: Math.min(64 * 1024, Math.floor(this.settingsManager.getCanonicalContextLimits().maxSourceBytes / 2)),
10096
+ probe: async (request) => {
10097
+ const kernel = await this._ipythonKernelProvisioner?.ensure();
10098
+ if (!kernel?.jobWatchProbe)
10099
+ throw new Error("Kernel job_watch_probe_v1 is unavailable");
10100
+ const result = await kernel.jobWatchProbe({
10101
+ watchId: request.id,
10102
+ generation: request.generation,
10103
+ resourceId: request.resource_id,
10104
+ jobId: request.job_id,
10105
+ completionSource: request.completion_source,
10106
+ command: request.command ?? null,
10107
+ timeoutMs: request.timeout_ms,
10108
+ });
10109
+ await controller.observe({ id: request.id, generation: request.generation, ...result });
10110
+ },
10111
+ cancelProbe: (id) => this._ipythonKernelProvisioner?.manager?.cancelJobWatchProbe?.(id),
10112
+ retain: async (output) => {
10113
+ const directory = manager.getSessionArtifactDir();
10114
+ if (!directory)
10115
+ return undefined;
10116
+ const retained = await retainToolOutput(directory, [output], false);
10117
+ return join(directory, retained.artifactId);
10118
+ },
10119
+ onError: (error) => this._surfaceSessionInputError(error),
10120
+ admit: (text, eventId) => {
10121
+ if (this._sessionInputAdmissionPauses.size)
10122
+ return false;
10123
+ const content = `[job-watch] ${text}`;
10124
+ const admission = this._admitSessionInput(this._createPreparedTurnAction("followUp", content, undefined, {
10125
+ message: {
10126
+ role: "custom",
10127
+ customType: "job_watch_event",
10128
+ content,
10129
+ display: true,
10130
+ timestamp: Date.now(),
10131
+ },
10132
+ queueKey: eventId,
10133
+ resumeIfIdle: !this._sessionInputPumpSuspended,
10134
+ }), { wake: !this._sessionInputPumpSuspended });
10135
+ if (admission.ticket)
10136
+ void admission.ticket.delivered
10137
+ .then((outcome) => outcome.status === "delivered"
10138
+ ? controller.acknowledge(eventId)
10139
+ : controller.releaseDelivery(eventId, true), () => controller.releaseDelivery(eventId, false))
10140
+ .catch((error) => this._surfaceSessionInputError(error));
10141
+ return admission.accepted;
10142
+ },
10143
+ });
10144
+ this._jobWatchController = controller;
10145
+ return controller;
10146
+ }
10018
10147
  _createKernelHostHandlers() {
10019
10148
  const handlers = {
10020
10149
  prime_context: async (payload, context) => {
@@ -10061,6 +10190,35 @@ export class AgentSession {
10061
10190
  handlers[type] = async (payload) => this.handleRlmHeartbeatHostRequest(type, payload);
10062
10191
  }
10063
10192
  }
10193
+ for (const type of ["watch", "status", "park", "unregister", "observation"]) {
10194
+ handlers[`job_watch.${type}`] = async (payload) => {
10195
+ if (this._disposed || this._disposing)
10196
+ throw new Error("Job watch source owner changed");
10197
+ if (type === "observation" && !this._jobWatchController?.isCurrent())
10198
+ return { ignored: true };
10199
+ const controller = this._getJobWatchController();
10200
+ switch (type) {
10201
+ case "watch":
10202
+ return { ...(await controller.watch(payload)) };
10203
+ case "status":
10204
+ return controller.status(typeof payload.id === "string" ? payload.id : undefined);
10205
+ case "park":
10206
+ if (!Array.isArray(payload.ids) || !payload.ids.every((id) => typeof id === "string"))
10207
+ throw new Error("park requires watch ids");
10208
+ return controller.park(payload.ids);
10209
+ case "unregister":
10210
+ return controller.unregister(String(payload.id));
10211
+ default:
10212
+ if (typeof payload.id !== "string" ||
10213
+ typeof payload.generation !== "string" ||
10214
+ !payload.result ||
10215
+ typeof payload.result !== "object")
10216
+ throw new Error("Invalid job watch observation");
10217
+ await controller.observe({ ...payload.result, id: payload.id, generation: payload.generation });
10218
+ return {};
10219
+ }
10220
+ };
10221
+ }
10064
10222
  const visibleKernelSkillNames = new Set(this._modelVisibleSkills()
10065
10223
  .filter((skill) => !skill.disableModelInvocation)
10066
10224
  .map((skill) => skill.name));