@awak-app/simy-cli 0.5.57 → 0.5.58

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.5.57",
3
+ "version": "0.5.58",
4
4
  "description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -378,8 +378,21 @@ export async function startAgent({
378
378
  });
379
379
 
380
380
  let providerReadInFlight = null;
381
+ let providerReadForced = false;
382
+ let providerForcedFollowup = null;
381
383
  const readProviderStatuses = ({ force = false } = {}) => {
382
- if (providerReadInFlight) return providerReadInFlight;
384
+ if (providerReadInFlight) {
385
+ if (!force || providerReadForced) return providerReadInFlight;
386
+ // A manual refresh must observe changes made after a background probe
387
+ // began. Queue one fresh batch without overlapping provider subprocesses.
388
+ if (!providerForcedFollowup) {
389
+ providerForcedFollowup = providerReadInFlight
390
+ .then(() => readProviderStatuses({ force: true }))
391
+ .finally(() => { providerForcedFollowup = null; });
392
+ }
393
+ return providerForcedFollowup;
394
+ }
395
+ providerReadForced = force;
383
396
  providerReadInFlight = (async () => {
384
397
  const previous = providerStatuses;
385
398
  await Promise.all(["codex", "claude"].map(async (provider) => {
@@ -400,7 +413,7 @@ export async function startAgent({
400
413
  refreshAvailableCapabilities();
401
414
  }
402
415
  return providerStatuses;
403
- })().finally(() => { providerReadInFlight = null; });
416
+ })().finally(() => { providerReadInFlight = null; providerReadForced = false; });
404
417
  return providerReadInFlight;
405
418
  };
406
419
 
@@ -13,7 +13,7 @@ import {
13
13
  } from "./durable-local-task-steps-contract.js";
14
14
  import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
15
15
 
16
- export const CLI_VERSION = "0.5.57";
16
+ export const CLI_VERSION = "0.5.58";
17
17
  export const CLI_API_CONTRACT_VERSION = 10;
18
18
 
19
19
  export function withCliContract(
@@ -37,7 +37,9 @@ export function createDesktopAccessPolicy({ readSession, isValid, fetchAccess, i
37
37
  if (base.state !== 'checking' || isSwitching()) return base;
38
38
  const session = readSession();
39
39
  if (pending?.session === session) return pending.promise;
40
- if (!force && readSession() === verifiedSession && now() - verifiedAt < ttlMs && verified) return snapshot();
40
+ if (!force && readSession() === verifiedSession && now() - verifiedAt < ttlMs - Math.min(5000, ttlMs / 5) && verified) return snapshot();
41
+ // Renew before expiry while the existing grant is still valid. snapshot()
42
+ // continues to expire closed at the original TTL if the network stalls.
41
43
  onChange();
42
44
  const job = { session, promise: null };
43
45
  job.promise = (async () => {
@@ -67,7 +67,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
67
67
  }
68
68
  const lock = candidate ? null : await acquireDesktopProcessLock(root, { webOrigin });
69
69
  const supervisorToken = lock?.nonce || env.SIMY_DESKTOP_SUPERVISOR_TOKEN;
70
- let agent, updater, analysisManager, automaticContinuation, stopWorkflows = async () => {}, timer, closed = false, relay = null, publishing = null, publishAgain = false;
70
+ let agent, updater, analysisManager, automaticContinuation, stopWorkflows = async () => {}, timer, closed = false, relay = null, publishing = null, publishAgain = false, scheduledPublish = null;
71
71
  const makeActionAssociations = () => (dependencies.createActionAssociationHost || createActionAssociationHost)({ root: join(root, "delivery-hooks"), sessionRoot: root, webOrigin, onChange: () => { if (!closed) setImmediate(() => { void publish().catch(() => {}); }); } });
72
72
  let actionAssociations = makeActionAssociations();
73
73
  let currentAccess = { state: 'checking', organizationId: null, organizationName: null };
@@ -125,12 +125,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
125
125
  }
126
126
  return sessionStatus;
127
127
  };
128
- const selectedSession = async command => {
128
+ const selectedSession = async (command, { reuseDiscovery = false } = {}) => {
129
129
  const scope = await authorizeWork();
130
- refreshSessions(true);
130
+ // Analysis joins an existing discovery instead of scheduling a second
131
+ // full scan. Enrollment retains its existing fresh-discovery boundary.
132
+ if (!reuseDiscovery || !sessionRefresh) refreshSessions(true);
131
133
  await sessionRefresh;
132
134
  // A forced refresh requested during discovery schedules one follow-up.
133
- if (sessionRefresh) await sessionRefresh;
135
+ if (!reuseDiscovery && sessionRefresh) await sessionRefresh;
134
136
  const candidate = sessionStatus.sessions.find(item => item.provider === command.provider && item.session === command.session);
135
137
  if (!candidate) throw new Error('This conversation is unavailable; refresh its provider.');
136
138
  await authorizeWork(scope);
@@ -162,7 +164,19 @@ export async function startDesktopHost({ input = process.stdin, output = process
162
164
  },
163
165
  onChange: () => { if (!closed && !relay) setImmediate(() => { void publish().catch(() => {}); }); },
164
166
  });
167
+ // Batch burst notifications and maintenance completions into one frame. The
168
+ // short window leaves commands responsive and the regular heartbeat intact.
165
169
  const publish = () => {
170
+ if (closed || relay || !agent) return Promise.resolve();
171
+ if (!scheduledPublish) {
172
+ let resolve, reject;
173
+ const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
174
+ scheduledPublish = { promise };
175
+ setTimeout(() => { scheduledPublish = null; projectStatus().then(resolve, reject); }, 75);
176
+ }
177
+ return scheduledPublish.promise;
178
+ };
179
+ const projectStatus = () => {
166
180
  if (closed || relay || !agent) return Promise.resolve();
167
181
  if (publishing) { publishAgain = true; return publishing; }
168
182
  publishing = Promise.resolve(agent.getDesktopStatus()).then(async (status) => {
@@ -471,8 +485,8 @@ export async function startDesktopHost({ input = process.stdin, output = process
471
485
  maxContinuations: command.maxContinuations, maxFailures: command.maxFailures, maxNoProgressMinutes: command.maxNoProgressMinutes, maxTokens: command.maxTokens });
472
486
  await refreshSessions(true);
473
487
  } else if (action === "session_analyze") {
474
- const candidate = await selectedSession(command);
475
- await analysisManager.request(candidate);
488
+ const candidate = await selectedSession(command, { reuseDiscovery: true });
489
+ await analysisManager.request(candidate, { requestId: command.analysisRequestId || command.id });
476
490
  if (candidate.autoContinue === true && candidate.capabilities?.canContinue === true) automaticContinuation.retry(candidate.provider, candidate.session);
477
491
  } else if (action === "session_continue") {
478
492
  const candidate = await selectedSession(command);
@@ -78,6 +78,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
78
78
  entries.set(key, { revision, analysisVersion: CONTINUATION_ANALYSIS_VERSION, cwd: candidate.cwd,
79
79
  forceAnalysis: forceAnalysis || entry?.forceAnalysis === true && ['queued', 'analyzing'].includes(entry.result?.state),
80
80
  settledResult: entry?.cwd === candidate.cwd ? entry.settledResult : null,
81
+ requestId: entry?.cwd === candidate.cwd && ['queued', 'analyzing'].includes(entry.result?.state) ? entry.requestId : undefined,
81
82
  result: { state: 'queued' } });
82
83
  return true;
83
84
  };
@@ -135,7 +136,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
135
136
  }
136
137
  const settledResult = finishedAnalysis(result, selected.previousAnalysis || entry.settledResult);
137
138
  entries.set(selected.key, { revision: selected.revision, analysisVersion: CONTINUATION_ANALYSIS_VERSION, cwd: current.cwd, settledResult,
138
- result: { ...result, analyzedAt: settledResult?.analyzedAt || now() } });
139
+ requestId: selected.entry.requestId, result: { ...result, analyzedAt: settledResult?.analyzedAt || now() } });
139
140
  await persist(); changed();
140
141
  });
141
142
  };
@@ -149,7 +150,9 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
149
150
  const selected = await exclusive(async () => {
150
151
  if (closed) return [];
151
152
  const batch = [];
152
- for (const [key, entry] of entries) {
153
+ // Explicit retries precede historical backfill without interrupting owned jobs.
154
+ const ordered = [...entries].sort((a, b) => Number(Boolean(b[1].requestId)) - Number(Boolean(a[1].requestId)));
155
+ for (const [key, entry] of ordered) {
153
156
  if (jobs.size >= concurrency) break;
154
157
  if (!runnable(key, entry)) continue;
155
158
  const candidate = candidates.get(key), controller = new AbortController();
@@ -233,7 +236,7 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
233
236
  },
234
237
  project(candidate) {
235
238
  const revision = continuationAnalysisRevision(candidate), entry = entries.get(keyOf(candidate));
236
- let analysis = entry?.revision === revision ? { ...entry.result, revision } : { state: 'unanalysed', revision };
239
+ let analysis = entry?.revision === revision ? { ...entry.result, revision, requestId: entry.requestId } : { state: 'unanalysed', revision };
237
240
  const previous = entry?.cwd === candidate.cwd ? entry.settledResult : null;
238
241
  if (candidate.isRunning === true || analysis.awaitingTurn === true) {
239
242
  analysis = { state: 'waiting', awaitingTurn: true, revision,
@@ -244,12 +247,16 @@ export async function createContinuationAnalysisManager({ root, analyze = analyz
244
247
  }
245
248
  return { ...candidate, analysis };
246
249
  },
247
- async request(candidate) {
250
+ async request(candidate, { requestId } = {}) {
251
+ if (requestId !== undefined && (typeof requestId !== 'string' || !requestId || requestId.length > 128)) throw new Error('Invalid analysis request identity.');
248
252
  if (!candidate || !eligible(candidate)) throw new Error('This conversation cannot be analyzed.');
249
253
  await exclusive(async () => {
250
254
  if (closed) throw new Error('Session analysis is unavailable.');
251
255
  candidates.set(keyOf(candidate), candidate);
252
- if (enqueue(candidate, true, true)) { await persist(); changed(); }
256
+ let dirty = enqueue(candidate, true, true);
257
+ const entry = entries.get(keyOf(candidate));
258
+ if (requestId && entry.requestId !== requestId) { entry.requestId = requestId; dirty = true; }
259
+ if (dirty) { await persist(); changed(); }
253
260
  });
254
261
  startDrain();
255
262
  },