@awak-app/simy-cli 0.7.47 → 0.7.49

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awak-app/simy-cli",
3
- "version": "0.7.47",
3
+ "version": "0.7.49",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@ import {
14
14
  } from "./durable-local-task-steps-contract.js";
15
15
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
16
16
 
17
- export const CLI_VERSION = "0.7.47";
17
+ export const CLI_VERSION = "0.7.49";
18
18
  export const CLI_API_CONTRACT_VERSION = 10;
19
19
 
20
20
  export function narrativeImageCapabilityMetadata() {
@@ -691,6 +691,23 @@ export function createManagedSupervisor(options) {
691
691
  // Continue actual task work and its result checks. Starting an unrelated
692
692
  // periodic audit here could acquire a lease that prevents that work.
693
693
  if (!running) running = (async () => {
694
+ // Share cancellation discovery within this offered batch, never its
695
+ // authority: reconciliation still reads each current record. Long-lived
696
+ // queued jobs refresh at the desktop's 3s maintenance cadence; new ticks
697
+ // also replace their queued observations. Failed reads remain retryable.
698
+ let cancellationCandidates, cancellationReadAt;
699
+ const readCancellationRecords = () => {
700
+ const age = Date.now() - cancellationReadAt;
701
+ if (!cancellationCandidates || !(age >= 0 && age < 3000)) {
702
+ cancellationReadAt = Date.now();
703
+ const pending = Promise.resolve()
704
+ .then(() => options.readCancellationRecords ? options.readCancellationRecords() : listRecords(options.root))
705
+ .then(records => records.filter(record => record.run?.managed?.nativeHandoff));
706
+ cancellationCandidates = pending;
707
+ void pending.catch(() => { if (cancellationCandidates === pending) cancellationCandidates = null; });
708
+ }
709
+ return cancellationCandidates;
710
+ };
694
711
  for (const record of await readCandidates()) {
695
712
  if (closed || await options.shouldYield?.()) break;
696
713
  if (!record.run?.managed) continue;
@@ -707,7 +724,7 @@ export function createManagedSupervisor(options) {
707
724
  return rows.filter(row => row?.provider === record.provider && row.session === record.session &&
708
725
  row.run?.id === record.run.id && row.run?.managed?.taskId === record.run.managed.taskId);
709
726
  };
710
- await tickManagedTasks({ ...options, readRecords, signal: controller.signal });
727
+ await tickManagedTasks({ ...options, readRecords, readCancellationRecords, signal: controller.signal });
711
728
  });
712
729
  }
713
730
  })().finally(() => { running = null; });
@@ -1053,7 +1053,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
1053
1053
  setImmediate(() => { void publish().catch(() => {}); });
1054
1054
  } },
1055
1055
  });
1056
- // Batch burst notifications and maintenance completions into one frame. The
1056
+ // Batch burst notifications and heartbeat/status requests into one frame. The
1057
1057
  // short window leaves commands responsive and the regular heartbeat intact.
1058
1058
  const publish = () => {
1059
1059
  if (closed || relay || !agent) return Promise.resolve();
@@ -1544,7 +1544,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
1544
1544
  const maintenance = new Map();
1545
1545
  const tick = worker => {
1546
1546
  if (closed || relay || !coordinator.acceptsNewWork() || maintenance.has(worker)) return;
1547
- const pending = Promise.resolve().then(() => worker.tick()).then(() => { if (!closed && !relay) return publish(); }).catch(() => {}).finally(() => maintenance.delete(worker));
1547
+ // The existing heartbeat publishes completed background maintenance.
1548
+ // Completion itself does not prove a visible change; after the short
1549
+ // batching window it otherwise reprojects/repaints the entire history.
1550
+ // Explicit commands and actual change notifications keep their live path.
1551
+ const pending = Promise.resolve().then(() => worker.tick()).catch(() => {}).finally(() => maintenance.delete(worker));
1548
1552
  maintenance.set(worker, pending);
1549
1553
  };
1550
1554
  tick(actionAssociations);
@@ -36,6 +36,14 @@ export async function worktreeEvidence(worktree) {
36
36
  }
37
37
  const stopped = new Set(['stopped','paused','paused_by_user','cancelled']);
38
38
  const terminal = new Set(['completed','reported_done','failed','succeeded']);
39
+ const terminalLocalTasks = new Set(['succeeded','failed','stopped']);
40
+ function hasPendingExecution(task, localTask = false) {
41
+ // Coding runs use operation for an async handle. Local Tasks persist their
42
+ // operation kind (for example "read") even after completion and restoration.
43
+ const operation = localTask ? task.operation != null && typeof task.operation !== 'string' : task.operation;
44
+ return Boolean(task.child || operation || task.operation_promise || task.pendingLedgerUpdate || task.ledgerUpdateRunning ||
45
+ (localTask ? !terminalLocalTasks.has(task.status) : ['queued','running','dispatching','coding'].includes(task.status)));
46
+ }
39
47
  const taskState = run => stopped.has(run.state) ? run.state : run.managed?.pendingApproval ? 'approval_wait' : ['blocked','waiting_human','recovery_required'].includes(run.state) ? 'needs_user' : 'active';
40
48
  const originalTaskId = record => record.run.managed?.taskId || record.run.id;
41
49
  const authority = record => digest([record.run?.id,record.run?.state,record.run?.managed,record.run?.deadline,record.run?.continuations,record.run?.failures,record.autoContinue,record.process,record.cwd,record.run?.executionCwd]);
@@ -167,7 +175,8 @@ export function createUpdateRuntime({ root, origin, readContext, listRecords, ob
167
175
  if(startingGate.connection!=='detaching')return {safe:false,liveExecutions:0,pendingWrites:0,unsettledPermissions:0};
168
176
  retainer?.assertSupported([]);
169
177
  const c=context(), execution=[...(c.runs||[]),...(c.localTasks||[])];
170
- const unowned=execution.filter(run=>run.child || run.operation || run.pendingLedgerUpdate || run.ledgerUpdateRunning || ['queued','running','dispatching','coding'].includes(run.status));
178
+ const unowned=[...(c.runs||[]).filter(run=>hasPendingExecution(run)),
179
+ ...(c.localTasks||[]).filter(task=>hasPendingExecution(task,true))];
171
180
  const workers=[];
172
181
  for(const task of c.directTasks || []) {
173
182
  if(!['queued','running','stopping'].includes(task.status)) continue;