@nanhara/hara 0.124.3 → 0.124.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to `@nanhara/hara`.
5
5
  > Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
6
6
  > **patch** (last) number bumps for **optimizations/fixes of existing features**.
7
7
 
8
+ ## 0.124.4 — 2026-07-18 — deadline-aware task checkpoints
9
+
10
+ - **The 80% run-budget warning now reaches the Agent, not only the user.** Before the next model turn,
11
+ Hara injects a hidden checkpoint instruction to finish the current atomic step, persist artifacts,
12
+ update the checklist, and defer a new multi-minute stage to `/continue`. A video/image batch, full
13
+ validation, preview, render, install, or deployment should no longer begin with too little turn budget
14
+ and then appear to stop mysteriously at the 30-minute safety pause.
15
+
8
16
  ## 0.124.3 — 2026-07-17 — project-aware resume and runtime hardening
9
17
 
10
18
  - **Saved sessions now resume in the project they belong to.** `hara resume <id>` validates the persisted
@@ -149,6 +149,13 @@ function composeSystem(cwd, projectContext, override, memory, continuationSessio
149
149
  }
150
150
  const RUN_STOPPED = Symbol("agent-run-stopped");
151
151
  const REPEATED_FAILURE_LIMIT = 3;
152
+ export function deadlineCheckpointReminder(timeoutMs) {
153
+ return (`Turn budget checkpoint: about 20% remains before the ${formatAgentDuration(timeoutMs)} safety pause. ` +
154
+ "Stop expanding scope. Finish only the current atomic step, persist any usable artifact, update todo_write, " +
155
+ "and reply with the completed checkpoint plus the next exact step. Do not start another generation batch, " +
156
+ "install, full validation suite, preview, render, deployment, or other multi-minute stage in this turn. " +
157
+ "The user can run /continue to start that next stage with a fresh bounded budget.");
158
+ }
152
159
  function showRunNotice(opts, message, critical = false) {
153
160
  if (opts.quiet)
154
161
  return;
@@ -164,6 +171,13 @@ function showRunNotice(opts, message, critical = false) {
164
171
  }
165
172
  }
166
173
  }
174
+ function requestRunCheckpoint(opts, life) {
175
+ if (life.disposed || life.checkpointDue || life.signal.aborted)
176
+ return;
177
+ life.checkpointDue = true;
178
+ const remainingMs = Math.max(0, life.timeoutMs - (Date.now() - life.startedAt));
179
+ showRunNotice(opts, `⚠ agent turn nearing its safety pause: ${formatAgentDuration(remainingMs)} remains. The agent will be told to finish the current atomic step and checkpoint; use \`/continue\` for the next expensive stage.`);
180
+ }
167
181
  function warnRun(opts, life) {
168
182
  if (life.disposed || life.warned || life.signal.aborted)
169
183
  return;
@@ -191,6 +205,9 @@ function createRunLifecycle(opts) {
191
205
  const warningDelay = Math.min(5 * 60_000, Math.max(250, Math.floor(timeoutMs * 0.8)));
192
206
  const warningTimer = setTimeout(() => warnRun(opts, life), warningDelay);
193
207
  warningTimer.unref?.();
208
+ const checkpointDelay = Math.max(250, Math.floor(timeoutMs * 0.8));
209
+ const checkpointTimer = setTimeout(() => requestRunCheckpoint(opts, life), checkpointDelay);
210
+ checkpointTimer.unref?.();
194
211
  let removeStopListener = () => { };
195
212
  const stopPromise = new Promise((resolveStopped) => {
196
213
  const stopped = () => resolveStopped(RUN_STOPPED);
@@ -205,6 +222,7 @@ function createRunLifecycle(opts) {
205
222
  timeoutController,
206
223
  timeoutTimer,
207
224
  warningTimer,
225
+ checkpointTimer,
208
226
  stopPromise,
209
227
  removeStopListener,
210
228
  startedAt,
@@ -213,6 +231,8 @@ function createRunLifecycle(opts) {
213
231
  rounds: 0,
214
232
  timedOut: false,
215
233
  warned: false,
234
+ checkpointDue: false,
235
+ checkpointInjected: false,
216
236
  limitAnnounced: false,
217
237
  disposed: false,
218
238
  failedCalls: new Map(),
@@ -223,6 +243,7 @@ function disposeRunLifecycle(life) {
223
243
  life.disposed = true;
224
244
  clearTimeout(life.timeoutTimer);
225
245
  clearTimeout(life.warningTimer);
246
+ clearTimeout(life.checkpointTimer);
226
247
  life.removeStopListener();
227
248
  }
228
249
  function hardStop(opts, life, kind, detail) {
@@ -325,6 +346,15 @@ async function runAgentInner(history, opts, life) {
325
346
  life.rounds += 1;
326
347
  if (life.rounds >= Math.ceil(life.maxRounds * 0.75))
327
348
  warnRun(opts, life);
349
+ if (Date.now() - life.startedAt >= Math.floor(life.timeoutMs * 0.8))
350
+ requestRunCheckpoint(opts, life);
351
+ if (!opts.quiet && life.checkpointDue && !life.checkpointInjected) {
352
+ life.checkpointInjected = true;
353
+ history.push({
354
+ role: "user",
355
+ content: wrapReminders([deadlineCheckpointReminder(life.timeoutMs)]),
356
+ });
357
+ }
328
358
  // Type-ahead steering: fold in anything the user submitted while the previous step ran, so it
329
359
  // reaches the model on this next call (drained after the last tool round; empty on the 1st pass).
330
360
  if (opts.pendingInput && !runSignal.aborted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.124.3",
3
+ "version": "0.124.4",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "runtime-bootstrap.cjs"