@awak-app/simy-cli 0.5.53 → 0.5.55
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 +1 -1
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/hooks/delivery-loop.mjs +7 -0
- package/src/delivery-hooks/supervisor.js +5 -8
- package/src/desktop-host.js +3 -2
- package/src/session-supervision/action-association-host.js +22 -10
- package/src/session-supervision/action-deadline-store.js +5 -4
- package/src/session-supervision/action-deadline.js +4 -1
- package/src/session-supervision/association-store.js +4 -8
- package/src/session-supervision/codex-app-server.js +17 -5
- package/src/session-supervision/index.js +5 -5
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -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.
|
|
16
|
+
export const CLI_VERSION = "0.5.55";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
|
@@ -121,6 +121,13 @@ export async function updateRecord(root, provider, session, mutate, { inspectPro
|
|
|
121
121
|
return next;
|
|
122
122
|
} finally { if (temporary) await rm(temporary, { force: true }); await releaseStateLock(lock, lockToken); }
|
|
123
123
|
}
|
|
124
|
+
export async function readRecord(root = rootDirectory(), provider, session) {
|
|
125
|
+
const path = recordPath(root, provider, session);
|
|
126
|
+
try {
|
|
127
|
+
const record = await read(path);
|
|
128
|
+
return record?.schemaVersion === 1 && record.provider === provider && record.session === session ? record : null;
|
|
129
|
+
} catch { return null; } // Match listRecords' treatment of unreadable records.
|
|
130
|
+
}
|
|
124
131
|
export async function listRecords(root = rootDirectory()) {
|
|
125
132
|
let names; try { names = await readdir(root); } catch (e) { if (e.code === 'ENOENT') return []; throw e; }
|
|
126
133
|
const results = await Promise.all(names.filter(n => /^[a-f0-9]{64}\.json$/.test(n)).slice(0, 500).map(async n => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
|
-
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, listRecords, managedBudgetReason, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
|
+
import { automaticContinuationAllowed, assessedCheckpointContract, checkpointDelivery, decideStop, listRecords, readRecord, managedBudgetReason, notifyState, processIdentity, reconcileRecords, updateRecord, verifyAutomaticCriteria } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
5
5
|
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
6
6
|
import { executeCodexAppServer } from '../session-supervision/codex-app-server.js';
|
|
7
7
|
import { acquireWorkspaceLease, leaseOwnerDead } from '../session-supervision/workspace-lease.js';
|
|
@@ -20,7 +20,7 @@ function providerFailureReason(code) {
|
|
|
20
20
|
}
|
|
21
21
|
async function assertActiveLease(root, provider, session, leaseId, signal, currentTime = Date.now) {
|
|
22
22
|
if (signal?.aborted) throw new Error('Managed recovery cancelled before provider dispatch.');
|
|
23
|
-
const current =
|
|
23
|
+
const current = await readRecord(root, provider, session);
|
|
24
24
|
if (signal?.aborted || current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId) {
|
|
25
25
|
throw new Error('Managed recovery lease changed before provider dispatch.');
|
|
26
26
|
}
|
|
@@ -95,8 +95,7 @@ export async function executeCodexResume(record, { signal, root, leaseId, execut
|
|
|
95
95
|
};
|
|
96
96
|
signal?.addEventListener('abort', terminate, { once: true });
|
|
97
97
|
const watch = setInterval(() => {
|
|
98
|
-
void
|
|
99
|
-
const current = records.find(item => item.provider === record.provider && item.session === record.session);
|
|
98
|
+
void readRecord(root, record.provider, record.session).then(current => {
|
|
100
99
|
if (current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId) terminate();
|
|
101
100
|
}).catch(terminate);
|
|
102
101
|
}, 500);
|
|
@@ -123,9 +122,8 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
123
122
|
const tickWallStart = Date.now();
|
|
124
123
|
const currentTime = () => now + Math.max(0, Date.now() - tickWallStart);
|
|
125
124
|
if (signal?.aborted) return;
|
|
126
|
-
await reconcileRecords({ root, now, ...(inspectProcess ? { inspectProcess } : {}) });
|
|
125
|
+
const records = await reconcileRecords({ root, now, ...(inspectProcess ? { inspectProcess } : {}) });
|
|
127
126
|
if (signal?.aborted) return;
|
|
128
|
-
const records = await listRecords(root);
|
|
129
127
|
for (let record of records) {
|
|
130
128
|
if (signal?.aborted) break;
|
|
131
129
|
const claimNow = currentTime();
|
|
@@ -212,8 +210,7 @@ export async function tickManagedTasks({ root, now = Date.now(), execute, inspec
|
|
|
212
210
|
else signal?.addEventListener('abort', abort, { once: true });
|
|
213
211
|
const timeout = setTimeout(abort, Math.max(1, claimed.run.deadline - currentTime()));
|
|
214
212
|
const watch = setInterval(() => {
|
|
215
|
-
void
|
|
216
|
-
const current = records.find(item => item.provider === record.provider && item.session === record.session);
|
|
213
|
+
void readRecord(root, record.provider, record.session).then(current => {
|
|
217
214
|
if (current?.run?.state !== 'auto_resuming' || current.run.managed?.resumeLease?.id !== leaseId ||
|
|
218
215
|
managedBudgetReason(current.run, currentTime()) || !waitingForTool && currentTime() - lastActivityAt >= 5 * 60_000) controller.abort();
|
|
219
216
|
}).catch(() => controller.abort());
|
package/src/desktop-host.js
CHANGED
|
@@ -181,13 +181,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
status.deliveryHooks = await (dependencies.deliveryHookStatus || deliveryHookStatus)({ root: join(root, "delivery-hooks") }).catch(() => ({ error: "Delivery hook status unavailable", providers: {}, runs: [] }));
|
|
184
|
+
const projection = await actionAssociations.beginProjection?.();
|
|
184
185
|
await actionAssociations.decorate(status.deliveryHooks);
|
|
185
186
|
const discovery = refreshSessions();
|
|
186
187
|
automaticContinuation?.tick(discovery.sessions);
|
|
187
188
|
const sessions = discovery.sessions.map(candidate => automaticContinuation.project(analysisManager.project(candidate)));
|
|
188
|
-
await actionAssociations.decorateSessions?.(sessions);
|
|
189
|
+
await actionAssociations.decorateSessions?.(sessions, projection);
|
|
189
190
|
for (const session of sessions) {
|
|
190
|
-
const context = await actionAssociations.deadlineContext?.(session);
|
|
191
|
+
const context = await actionAssociations.deadlineContext?.(session, projection);
|
|
191
192
|
if (context) session.actionDeadline = await deadlines.reconcile(context);
|
|
192
193
|
else session.actionDeadline = { status: 'unavailable', online: false, reminder: null };
|
|
193
194
|
const run = status.deliveryHooks.runs?.find(item => item.provider === session.provider && item.session === session.session);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createOrdinaryAssociationStore } from './association-store.js';
|
|
2
|
-
import { listRecords, updateRecord, TERMINAL, managedBudgetReason } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
2
|
+
import { listRecords, readRecord, updateRecord, TERMINAL, managedBudgetReason } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
3
|
import { createActionAssociationApi, ActionAssociationApiError } from './action-association-api.js';
|
|
4
4
|
import { actionCandidateRequest, prepareActionAssociation, importActionBinding,
|
|
5
5
|
settleActionAssociation, cancelActionAssociation } from './action-association.js';
|
|
@@ -28,7 +28,7 @@ const deadlineBlocker = (record, candidate) => {
|
|
|
28
28
|
// Association is optional metadata. Network failures never control the task's
|
|
29
29
|
// work state; provider turns and their budgets remain owned by the supervisor.
|
|
30
30
|
export function createActionAssociationHost({ root, sessionRoot, webOrigin, api = createActionAssociationApi({ webOrigin, sessionRoot }),
|
|
31
|
-
now = Date.now, refreshMs = 60_000, list = listRecords, onChange = () => {} } = {}) {
|
|
31
|
+
now = Date.now, refreshMs = 60_000, list = listRecords, read = readRecord, onChange = () => {} } = {}) {
|
|
32
32
|
const abort = new AbortController(), views = new Map();
|
|
33
33
|
const ordinary = createOrdinaryAssociationStore({ root, api, webOrigin, now, onChange });
|
|
34
34
|
let auth = null, closed = false, running = null, serial = Promise.resolve();
|
|
@@ -52,7 +52,10 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
52
52
|
}
|
|
53
53
|
return next;
|
|
54
54
|
};
|
|
55
|
-
const fresh = async target =>
|
|
55
|
+
const fresh = async target => {
|
|
56
|
+
const record = await read(root, target.provider, target.session);
|
|
57
|
+
return eligible(record || {}) && record.run.managed.taskId === target.taskId ? record : null;
|
|
58
|
+
};
|
|
56
59
|
const valid = async expected => !closed && expected && await current() === expected;
|
|
57
60
|
const importEnvelope = async (record, envelope, expected, userCorrection = false) => {
|
|
58
61
|
if (!await valid(expected)) throw new ActionAssociationApiError('account_changed');
|
|
@@ -159,19 +162,28 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
159
162
|
}));
|
|
160
163
|
}
|
|
161
164
|
return {
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
// A projection is private to one desktop frame. Mutations and deadline
|
|
166
|
+
// validation still reread the exact record and current authentication.
|
|
167
|
+
async beginProjection() {
|
|
168
|
+
await current();
|
|
164
169
|
const records = await list(root);
|
|
165
|
-
|
|
166
|
-
|
|
170
|
+
return { records, byKey: new Map(records.map(record => [`${record.provider}:${record.session}`, record])) };
|
|
171
|
+
},
|
|
172
|
+
async decorateSessions(sessions, projection) {
|
|
173
|
+
await ordinary.decorate(sessions, projection);
|
|
174
|
+
const records = projection?.byKey || new Map((await list(root)).map(record => [`${record.provider}:${record.session}`, record]));
|
|
175
|
+
const status = { runs: sessions.filter(item => records.get(`${item.provider}:${item.session}`)?.run?.managed).map(item => {
|
|
176
|
+
const record = records.get(`${item.provider}:${item.session}`);
|
|
167
177
|
return { managed: true, taskId: record.run.managed.taskId, actionAssociation: record.run.managed.actionAssociation, candidate: item };
|
|
168
178
|
}) };
|
|
169
179
|
await this.decorate(status);
|
|
170
180
|
for (const run of status.runs) run.candidate.actionAssociation = run.actionAssociation;
|
|
171
181
|
return sessions;
|
|
172
182
|
},
|
|
173
|
-
async deadlineContext(candidate) {
|
|
174
|
-
const expected = await current(), record =
|
|
183
|
+
async deadlineContext(candidate, projection) {
|
|
184
|
+
const expected = await current(), record = projection
|
|
185
|
+
? projection.byKey.get(`${candidate.provider}:${candidate.session}`)
|
|
186
|
+
: await read(root, candidate.provider, candidate.session);
|
|
175
187
|
const local = record?.run?.managed?.actionAssociation || record?.actionAssociation;
|
|
176
188
|
if (!expected || local?.authRevision !== expected || !candidate.actionAssociation?.canCorrect) return null;
|
|
177
189
|
const binding = candidate.actionAssociation.binding;
|
|
@@ -181,7 +193,7 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
181
193
|
plan: record.run?.managed?.plan || candidate.analysis?.plan || null, blocker: deadlineBlocker(record, candidate), online: true,
|
|
182
194
|
validate: async () => {
|
|
183
195
|
if (!await valid(expected)) return false;
|
|
184
|
-
const latest =
|
|
196
|
+
const latest = await read(root, candidate.provider, candidate.session);
|
|
185
197
|
const scoped = latest?.run?.managed?.actionAssociation || latest?.actionAssociation;
|
|
186
198
|
return (latest?.run?.managed?.taskId || latest?.associationTaskId) === candidate.taskId && scoped?.namespace === local.namespace && scoped?.authRevision === expected && fingerprint(scoped.binding) === fingerprint(binding);
|
|
187
199
|
} };
|
|
@@ -47,10 +47,11 @@ export function createActionDeadlineStore({ root }) {
|
|
|
47
47
|
}
|
|
48
48
|
return next === undefined ? previous : next;
|
|
49
49
|
});
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
50
|
+
const tail = result.catch(() => {}).finally(() => {
|
|
51
|
+
// A newer transaction owns the queue until it too has settled.
|
|
52
|
+
if (queues.get(path) === tail) queues.delete(path);
|
|
53
|
+
});
|
|
54
|
+
queues.set(path, tail);
|
|
54
55
|
return result;
|
|
55
56
|
},
|
|
56
57
|
};
|
|
@@ -153,7 +153,6 @@ export function createActionDeadlineService({
|
|
|
153
153
|
async function reconcile(context) {
|
|
154
154
|
const key = identity(context),
|
|
155
155
|
action = context.binding?.action;
|
|
156
|
-
const dueInstant = actionDueInstant(action?.dueAt, action?.timeZone);
|
|
157
156
|
const bindingRevision =
|
|
158
157
|
context.binding?.revision ?? context.bindingRevision ?? null;
|
|
159
158
|
const fingerprint = hash([
|
|
@@ -176,6 +175,10 @@ export function createActionDeadlineService({
|
|
|
176
175
|
: previous.bindingRevision > bindingRevision)
|
|
177
176
|
)
|
|
178
177
|
return previous;
|
|
178
|
+
// An unchanged valid deadline already stores its exact due instant.
|
|
179
|
+
// Avoid rebuilding Intl formatters and repeating the timezone search.
|
|
180
|
+
if (previous?.fingerprint === fingerprint && previous.status !== 'unavailable') return previous;
|
|
181
|
+
const dueInstant = actionDueInstant(action?.dueAt, action?.timeZone);
|
|
179
182
|
// A pre-JST cache may mark the same date-only value unavailable. Repair
|
|
180
183
|
// that entry once without invalidating already valid saved decisions.
|
|
181
184
|
if (previous?.fingerprint === fingerprint && !(previous.status === "unavailable" && dueInstant !== null)) return previous;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import {
|
|
3
3
|
listRecords,
|
|
4
|
+
readRecord,
|
|
4
5
|
updateRecord,
|
|
5
6
|
} from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
6
7
|
import { associationRevision } from './action-association.js';
|
|
@@ -54,12 +55,7 @@ export function createOrdinaryAssociationStore({
|
|
|
54
55
|
abort = new AbortController();
|
|
55
56
|
let serial = Promise.resolve(),
|
|
56
57
|
closed = false;
|
|
57
|
-
const find =
|
|
58
|
-
(await listRecords(root)).find(
|
|
59
|
-
(item) =>
|
|
60
|
-
item.provider === candidate.provider &&
|
|
61
|
-
item.session === candidate.session,
|
|
62
|
-
);
|
|
58
|
+
const find = candidate => readRecord(root, candidate.provider, candidate.session);
|
|
63
59
|
async function ensure(candidate, known) {
|
|
64
60
|
if (
|
|
65
61
|
!['codex', 'claude'].includes(candidate.provider) ||
|
|
@@ -189,9 +185,9 @@ export function createOrdinaryAssociationStore({
|
|
|
189
185
|
}
|
|
190
186
|
}
|
|
191
187
|
return {
|
|
192
|
-
async decorate(sessions) {
|
|
188
|
+
async decorate(sessions, snapshot) {
|
|
193
189
|
const records = new Map(
|
|
194
|
-
(await listRecords(root)).map((record) => [
|
|
190
|
+
(snapshot?.records || await listRecords(root)).map((record) => [
|
|
195
191
|
`${record.provider}:${record.session}`,
|
|
196
192
|
record,
|
|
197
193
|
]),
|
|
@@ -157,12 +157,24 @@ export function codexThreadMetadata(thread) {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export async function listCodexSessions({ client, maxPages = 40, pageSize =
|
|
160
|
+
export async function listCodexSessions({ client, maxPages = 40, pageSize = 25, maxSessions = 200, inspectState = false, prioritySessions = [], observationBudgetMs = 3000 } = {}) {
|
|
161
161
|
const sessions = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
const windowSize = Math.min(maxSessions, maxPages * pageSize);
|
|
163
|
+
let cursor = null, listed = 0, pages = 0, batchSize = pageSize;
|
|
164
|
+
while (pages < Math.max(maxPages, Math.ceil(windowSize / batchSize)) && listed < windowSize) {
|
|
165
|
+
const limit = Math.min(batchSize, windowSize - listed);
|
|
166
|
+
let response;
|
|
167
|
+
try { response = await client.request('thread/list', { cursor, limit, sourceKinds: SOURCE_KINDS, archived: false }); }
|
|
168
|
+
catch (error) {
|
|
169
|
+
// Keep the transport's byte limit. Retry the same cursor in smaller
|
|
170
|
+
// batches rather than dropping discovery for large metadata envelopes.
|
|
171
|
+
if (error?.code !== 'response_too_large' || limit <= 1) throw error;
|
|
172
|
+
batchSize = Math.max(1, Math.floor(limit / 2));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const threads = (response?.data || []).slice(0, limit);
|
|
176
|
+
listed += threads.length; pages++;
|
|
177
|
+
for (const thread of threads) {
|
|
166
178
|
const metadata = codexThreadMetadata(thread);
|
|
167
179
|
if (metadata) sessions.push(metadata);
|
|
168
180
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { realpath } from 'node:fs/promises';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { TERMINAL as TERMINAL_RUNS, controlDelivery, listRecords, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
3
|
+
import { TERMINAL as TERMINAL_RUNS, controlDelivery, listRecords, readRecord, processIdentity, startDelivery, updateRecord } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
|
|
4
4
|
import { listCodexSessions, openCodexAppServer, readCodexSession } from './codex-app-server.js';
|
|
5
5
|
import { listClaudeSessions, readClaudeSession } from './claude-sessions.js';
|
|
6
6
|
import { originalSessionTarget } from './original-session-target.js';
|
|
@@ -133,7 +133,7 @@ export async function setSessionAutoContinue({ root, provider, session, enabled,
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
export async function readSessionAutoContinue({ root, provider, session }) {
|
|
136
|
-
const record =
|
|
136
|
+
const record = await readRecord(root, provider, session);
|
|
137
137
|
return { enabled: record?.autoContinue === true, epoch: record?.autoContinueEpoch || null,
|
|
138
138
|
settings: record?.autoContinueSettings, hasRun: Boolean(record?.run) };
|
|
139
139
|
}
|
|
@@ -190,14 +190,14 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
190
190
|
let metadata;
|
|
191
191
|
if (provider === 'claude') {
|
|
192
192
|
metadata = await readClaude(session);
|
|
193
|
-
const record =
|
|
193
|
+
const record = await readRecord(root, provider, session);
|
|
194
194
|
if (!await liveClaudeHook(record, metadata, inspectProcess)) throw new Error('Claude needs a live matching SIMY SessionStart hook; cold CLI takeover is unavailable.');
|
|
195
195
|
if (record.run) throw new Error('This session already has a Delivery Loop.');
|
|
196
196
|
await monitorSession({ root, provider, session, monitor: true, readClaude: async () => metadata, now });
|
|
197
197
|
// A second check narrows the time between metadata admission and the
|
|
198
198
|
// original provider's next Stop boundary. Process exit still remains a
|
|
199
199
|
// manual recovery condition; SIMY never starts claude -r.
|
|
200
|
-
if (!await liveClaudeHook(
|
|
200
|
+
if (!await liveClaudeHook(await readRecord(root, provider, session), metadata, inspectProcess)) throw new Error('Claude provider process exited before enrollment.');
|
|
201
201
|
} else {
|
|
202
202
|
metadata = await monitorSession({ root, provider, session, monitor: true, openClient, now });
|
|
203
203
|
if (!metadata.model) throw new Error('Codex did not expose this conversation’s model; automatic continuation is unavailable.');
|
|
@@ -205,7 +205,7 @@ export async function enrollSession({ root, provider, session, objective, accept
|
|
|
205
205
|
const cwd = await realpath(metadata.cwd);
|
|
206
206
|
const initialPlan = initialDiagnosisPlan(objective.trim(), contextBootstrap);
|
|
207
207
|
const admission = provider === 'codex' ? { source: 'codex_app_server', provider, session, cwd, model: metadata.model } : undefined;
|
|
208
|
-
const liveRecord = provider === 'claude' ?
|
|
208
|
+
const liveRecord = provider === 'claude' ? await readRecord(root, provider, session) : null;
|
|
209
209
|
const run = await startDelivery({ root, provider, session, cwd, objective: objective.trim(), nextStep: 'Inspect current evidence and propose the next assessed action.',
|
|
210
210
|
maxMinutes, maxContinuations, maxFailures, now, admission, inspectProcess, autoContinueEpoch,
|
|
211
211
|
liveHookAdmission: liveRecord ? { hookStartedAt: liveRecord.hookStartedAt, pid: liveRecord.process.pid, identity: liveRecord.process.identity } : undefined,
|