@awak-app/simy-cli 0.7.17 → 0.7.18
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/desktop-host.js +31 -4
- package/src/session-supervision/claude-sessions.js +5 -3
- package/src/session-supervision/codex-app-server.js +1 -0
- package/src/session-supervision/discovery-window.js +1 -1
- package/src/session-supervision/index.js +3 -3
- package/src/session-supervision/remote-hosts.js +1 -1
- package/src/team-threads.js +37 -16
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.7.
|
|
16
|
+
export const CLI_VERSION = "0.7.18";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function narrativeImageCapabilityMetadata() {
|
package/src/desktop-host.js
CHANGED
|
@@ -773,7 +773,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
773
773
|
for (const session of status.sessionSupervision.sessions) {
|
|
774
774
|
const key = teamThreadKey(agent.getTeamThreadsScope?.() || {}, session);
|
|
775
775
|
const own = status.teamThreads?.own.find(item => item.threadKey === key);
|
|
776
|
-
session.teamSharing = { enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true };
|
|
776
|
+
session.teamSharing = { threadKey: key, enabled: own?.enabled === true, pendingStop: status.teamThreads?.pendingStops.includes(key) === true, confirmationRequired: status.teamThreads?.confirmationRequired.includes(key) === true };
|
|
777
777
|
}
|
|
778
778
|
const latestAccess = await accessSnapshot();
|
|
779
779
|
if (JSON.stringify(await readProviderLinks(root)) !== requestedLinks ||
|
|
@@ -1138,9 +1138,20 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1138
1138
|
else if(action === 'team_thread_open') ok = await teamThreads.open({memberId:command.memberId,threadKey:command.threadKey});
|
|
1139
1139
|
else {
|
|
1140
1140
|
if (command.organizationId !== undefined && command.organizationId !== access.organizationId) throw new Error('Sharing organization changed. Confirm again.');
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1141
|
+
// Publication reads this host's provider context. Never reinterpret
|
|
1142
|
+
// a remote or ambiguous identity as a same-ID local conversation.
|
|
1143
|
+
if ((command.hostId && command.hostId !== 'local') || (!command.hostId && remoteHosts?.hasSession(command.session))) {
|
|
1144
|
+
throw new Error('Team sharing requires an explicitly selected local conversation.');
|
|
1145
|
+
}
|
|
1146
|
+
const selected = sessionStatus.sessions.find(item => (!item.hostId || item.hostId === 'local') && item.provider === command.provider && item.session === command.session);
|
|
1147
|
+
if (command.enabled === false) {
|
|
1148
|
+
// Revocation uses authenticated own rows, never provider context.
|
|
1149
|
+
const key = command.threadKey ?? teamThreadKey(agent.getTeamThreadsScope(), { provider: command.provider, session: command.session });
|
|
1150
|
+
ok = await teamThreads.stopSharing(key);
|
|
1151
|
+
} else {
|
|
1152
|
+
if(!selected || command.threadKey) throw new Error('Refresh and select your conversation again.');
|
|
1153
|
+
ok = await teamThreads.setSharing(selected,command.enabled);
|
|
1154
|
+
}
|
|
1144
1155
|
}
|
|
1145
1156
|
emit({type:'result',id:command.id,ok:ok===true}); void publish().catch(() => {}); return;
|
|
1146
1157
|
}
|
|
@@ -1457,6 +1468,22 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1457
1468
|
await authorizeWork(commandScope);
|
|
1458
1469
|
const result = await deadlines.decide(context, { ...command, requestId: command.requestId || command.decisionId });
|
|
1459
1470
|
if (result.status === 'conflict') throw Object.assign(new Error('Deadline changed; refresh before deciding.'), { code: 'conflict' });
|
|
1471
|
+
} else if (action === "session_set_pinned") {
|
|
1472
|
+
if (typeof command.enabled !== 'boolean') throw new Error('Choose whether to pin this conversation.');
|
|
1473
|
+
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1474
|
+
await authorizeWork(commandScope);
|
|
1475
|
+
const pinScope = command.pinScope ?? 'legacy';
|
|
1476
|
+
if (typeof pinScope !== 'string' || !pinScope || pinScope.length > 512) throw new Error('Invalid pin scope.');
|
|
1477
|
+
await updateRecord(deliveryRoot, command.provider, command.session, previous => {
|
|
1478
|
+
const scopes = new Set(Array.isArray(previous?.listPinScopes) ? previous.listPinScopes : previous?.listPinned ? ['legacy'] : []);
|
|
1479
|
+
if (command.enabled) scopes.add(pinScope); else scopes.delete(pinScope);
|
|
1480
|
+
return {
|
|
1481
|
+
...(previous || { schemaVersion: 1, provider: command.provider, session: command.session, cwd: candidate.cwd, observedAt: Date.now() }),
|
|
1482
|
+
listPinned: scopes.size > 0, listPinScopes: [...scopes],
|
|
1483
|
+
...(command.enabled ? { listMetadata: { title: candidate.title || '', createdAt: candidate.createdAt ?? null, lastActivityAt: candidate.lastActivityAt ?? null } } : {}),
|
|
1484
|
+
};
|
|
1485
|
+
});
|
|
1486
|
+
refreshSessions(true);
|
|
1460
1487
|
} else if (action === "session_archive" || action === "session_restore") {
|
|
1461
1488
|
const candidate = await selectedSession(command, { preferObserved: true });
|
|
1462
1489
|
await authorizeWork(commandScope);
|
|
@@ -25,7 +25,7 @@ function rememberMetadata(path, value) {
|
|
|
25
25
|
function candidateMetadata(id, metadata, lastActivityAt, observedAt) {
|
|
26
26
|
return {
|
|
27
27
|
provider: 'claude', session: id, ...(metadata.parentSessionId ? { parentSession: { provider: 'claude', session: metadata.parentSessionId } } : {}), cwd: metadata.cwd, title: metadata.title,
|
|
28
|
-
source: 'claude_cli', lastActivityAt,
|
|
28
|
+
source: 'claude_cli', lastActivityAt, createdAt: metadata.createdAt ?? null,
|
|
29
29
|
observation: { source: 'claude_local_index', observedAt },
|
|
30
30
|
availability: 'unsupported', model: null,
|
|
31
31
|
capabilities: { canMonitor: true, canEnroll: false, canAutoContinue: false },
|
|
@@ -62,7 +62,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
62
62
|
const text = bytes.toString('utf8');
|
|
63
63
|
const complete = text.slice(0, text.lastIndexOf('\n') + 1);
|
|
64
64
|
if (!complete) return null;
|
|
65
|
-
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false;
|
|
65
|
+
let cwd = '', title = '', customTitle = '', parentSessionId = null, found = false, createdAt = null;
|
|
66
66
|
for (const line of complete.split('\n').slice(0, MAX_LINES)) {
|
|
67
67
|
if (!line) continue;
|
|
68
68
|
let item;
|
|
@@ -73,6 +73,8 @@ function metadataFromPrefix(bytes, id) {
|
|
|
73
73
|
if (item.type === 'custom-title' && item.isSidechain !== true) { customTitle = sessionTitle(item.customTitle); continue; }
|
|
74
74
|
if (item.type !== 'user' && item.type !== 'assistant') continue;
|
|
75
75
|
if (item.isSidechain === true) return null;
|
|
76
|
+
const timestamp = typeof item.timestamp === 'string' ? Date.parse(item.timestamp) : item.timestamp;
|
|
77
|
+
if (Number.isFinite(timestamp) && timestamp > 0) createdAt = createdAt === null ? timestamp : Math.min(createdAt, timestamp);
|
|
76
78
|
if (typeof item.cwd !== 'string' || !isAbsolute(item.cwd)) continue;
|
|
77
79
|
if (!cwd) cwd = resolve(item.cwd);
|
|
78
80
|
// A conversation can cd into a child directory without changing identity.
|
|
@@ -80,7 +82,7 @@ function metadataFromPrefix(bytes, id) {
|
|
|
80
82
|
if (!title) title = sessionTitle(item.slug);
|
|
81
83
|
found = true;
|
|
82
84
|
}
|
|
83
|
-
return found ? { cwd, title: customTitle || title, parentSessionId } : null;
|
|
85
|
+
return found ? { cwd, title: customTitle || title, parentSessionId, createdAt } : null;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// /rename appends a custom-title envelope, often beyond the bounded prefix.
|
|
@@ -222,6 +222,7 @@ export function codexThreadMetadata(thread) {
|
|
|
222
222
|
title: typeof thread.name === 'string' && thread.name.trim() ? thread.name.slice(0, 200) : '',
|
|
223
223
|
source: typeof thread.source === 'string' ? thread.source : thread.source?.type || 'unknown',
|
|
224
224
|
lastActivityAt: Number.isFinite(thread.updatedAt) ? thread.updatedAt * 1000 : null,
|
|
225
|
+
createdAt: Number.isFinite(thread.createdAt) && thread.createdAt > 0 ? thread.createdAt * 1000 : null,
|
|
225
226
|
model: typeof thread.model === 'string' ? thread.model : null,
|
|
226
227
|
// Preserve native wait flags for presentation without changing isRunning,
|
|
227
228
|
// which also protects the provider writer lock. Unknown flags fail closed.
|
|
@@ -4,7 +4,7 @@ export const RECENT_SESSION_LIMIT = 200;
|
|
|
4
4
|
|
|
5
5
|
// Explicit tracking and unsettled work remain visible outside recent history.
|
|
6
6
|
// Completed, untracked records are history, not an unlimited extra catalog.
|
|
7
|
-
export const retainedSession = record => Boolean(record?.autoContinue || record?.monitor || record?.tracked ||
|
|
7
|
+
export const retainedSession = record => Boolean(record?.listPinned || record?.autoContinue || record?.monitor || record?.tracked ||
|
|
8
8
|
record?.run && !TERMINAL.has(record.run.state));
|
|
9
9
|
|
|
10
10
|
export function recentSessionWindow(items, { limit = RECENT_SESSION_LIMIT, priority = () => false } = {}) {
|
|
@@ -231,8 +231,8 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
231
231
|
// A hook observation can belong to an AI-created worker. Only explicit
|
|
232
232
|
// supervision may restore a conversation absent from provider discovery.
|
|
233
233
|
if (!byKey.has(key) && (retainedSession(record) || includeArchived && record.listHidden === true || requiredAncestors.has(key) && (record.observedAt || record.admission?.validatedAt))) byKey.set(key, {
|
|
234
|
-
provider: record.provider, session: record.session, cwd: record.cwd, title: '', archived, source: record.admission?.source || 'hook',
|
|
235
|
-
lastActivityAt: record.observedAt
|
|
234
|
+
provider: record.provider, session: record.session, cwd: record.cwd, title: record.listMetadata?.title || '', createdAt: record.listMetadata?.createdAt ?? null, archived, source: record.admission?.source || 'hook',
|
|
235
|
+
lastActivityAt: record.listMetadata?.lastActivityAt ?? record.observedAt ?? record.admission?.validatedAt ?? record.run?.updatedAt ?? null,
|
|
236
236
|
observation: { source: record.admission?.source || `${record.provider}_hook`, observedAt: record.observedAt || record.admission?.validatedAt || null },
|
|
237
237
|
availability: 'unknown', model: record.admission?.model || null,
|
|
238
238
|
});
|
|
@@ -327,7 +327,7 @@ export async function discoverSessions({ root, openClient = openCodexAppServer,
|
|
|
327
327
|
autoContinueEpoch: record?.autoContinue === true ? record.autoContinueEpoch : null,
|
|
328
328
|
autoContinueAvailability: availability,
|
|
329
329
|
...autoContinueState(record, isRunning, availability),
|
|
330
|
-
lastActivityAt: item.lastActivityAt, observation, availability: item.availability,
|
|
330
|
+
lastActivityAt: item.lastActivityAt, createdAt: item.createdAt ?? null, observation, availability: item.availability,
|
|
331
331
|
taskId: run?.managed?.taskId || record?.associationTaskId || null,
|
|
332
332
|
binding: { mode, taskId: run?.managed?.taskId || record?.associationTaskId || null },
|
|
333
333
|
originalSessionTarget: originalSessionTarget(item.provider, item.session),
|
|
@@ -13,7 +13,7 @@ export const remoteHostId = alias => `remote-ssh-discovered:${alias}`;
|
|
|
13
13
|
const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
14
14
|
const bootstrap = 'import { realpathSync } from "node:fs"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; const entry = join(dirname(realpathSync(process.argv[1])), "session-supervision", "remote-entry.js"); await import(pathToFileURL(entry).href);';
|
|
15
15
|
export const REMOTE_HOST_COMMAND = `exec sh -lc ${shellQuote(`exec env SIMY_REMOTE_CHILD=1 SIMY_AUTO_UPDATE=0 node --input-type=module -e ${shellQuote(bootstrap)} "$(command -v simy)"`)}`;
|
|
16
|
-
const allowedActions = new Set(['refresh', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
|
|
16
|
+
const allowedActions = new Set(['refresh', 'session_set_pinned', 'session_statistics', 'session_archive', 'session_restore', 'session_track', 'session_untrack',
|
|
17
17
|
'session_monitor', 'session_unmonitor', 'session_enroll', 'session_set_auto_continue', 'session_analyze', 'session_continue', 'session_answer',
|
|
18
18
|
'session_deadline_decide', 'session_action_refresh', 'session_action_select', 'session_action_unlink',
|
|
19
19
|
'delivery_pause', 'delivery_stop', 'delivery_reject', 'delivery_resume', 'delivery_replan', 'delivery_approve',
|
package/src/team-threads.js
CHANGED
|
@@ -113,6 +113,7 @@ export function createTeamThreads({
|
|
|
113
113
|
selected = null,
|
|
114
114
|
memberFilter = null,
|
|
115
115
|
pending = new Map(),
|
|
116
|
+
publicationGrants = new Map(),
|
|
116
117
|
storageFailed = false,
|
|
117
118
|
lastAllowed = false;
|
|
118
119
|
function scope() {
|
|
@@ -122,11 +123,12 @@ export function createTeamThreads({
|
|
|
122
123
|
generation++;
|
|
123
124
|
const sameIdentity = scopeKey === current.key;
|
|
124
125
|
scopeKey = current.key;
|
|
125
|
-
const own = sameIdentity ? value.own : [];
|
|
126
|
+
const own = sameIdentity && current.authenticated ? value.own : [];
|
|
126
127
|
value = { ...empty(), own };
|
|
127
128
|
selected = null;
|
|
128
129
|
memberFilter = null;
|
|
129
130
|
if (!sameIdentity) pending = new Map();
|
|
131
|
+
publicationGrants.clear();
|
|
130
132
|
lastPoll = -Infinity;
|
|
131
133
|
}
|
|
132
134
|
lastAllowed = current.allowed;
|
|
@@ -135,7 +137,7 @@ export function createTeamThreads({
|
|
|
135
137
|
}
|
|
136
138
|
const snapshot = () => {
|
|
137
139
|
scope();
|
|
138
|
-
return structuredClone({ ...value, pendingStops: [...pending.keys()] });
|
|
140
|
+
return structuredClone({ ...value, pendingStops: [...pending.keys()], confirmationRequired: value.own.filter(row => row.enabled && publicationGrants.get(row.threadKey) !== row.version).map(row => row.threadKey) });
|
|
139
141
|
};
|
|
140
142
|
const fenceKey = (current, key) => digest(`${current.fenceKey}:${key}`);
|
|
141
143
|
async function run(fn, allowOff = false) {
|
|
@@ -272,6 +274,11 @@ export function createTeamThreads({
|
|
|
272
274
|
incomplete: result.incomplete,
|
|
273
275
|
};
|
|
274
276
|
}
|
|
277
|
+
function sourceObservedAt(candidate) {
|
|
278
|
+
const observed = candidate.observation?.observedAt;
|
|
279
|
+
if (!Number.isFinite(observed) || observed <= 0 || observed > now()) invalid();
|
|
280
|
+
return new Date(observed).toISOString();
|
|
281
|
+
}
|
|
275
282
|
async function payload(candidate, signal) {
|
|
276
283
|
const result = await readContext(candidate, { signal });
|
|
277
284
|
if (!Array.isArray(result?.messages) || !result.messages.length) invalid();
|
|
@@ -295,7 +302,7 @@ export function createTeamThreads({
|
|
|
295
302
|
240,
|
|
296
303
|
),
|
|
297
304
|
state: running ? "running" : "unknown",
|
|
298
|
-
observedAt:
|
|
305
|
+
observedAt: sourceObservedAt(candidate),
|
|
299
306
|
messages,
|
|
300
307
|
truncated: result.truncated === true,
|
|
301
308
|
incomplete: result.incomplete === true,
|
|
@@ -322,13 +329,18 @@ export function createTeamThreads({
|
|
|
322
329
|
const fence = await fenceStore.read(fenceKey(current, item.threadKey));
|
|
323
330
|
if (!valid()) return;
|
|
324
331
|
if (fence) {
|
|
325
|
-
|
|
326
|
-
|
|
332
|
+
// A lost OFF response (or intervening version) must reconcile against
|
|
333
|
+
// authenticated current state, rather than retry a stale CAS forever.
|
|
334
|
+
pending.set(item.threadKey, item.version);
|
|
335
|
+
if (item.enabled) await stop(send, current, item.threadKey, item.version);
|
|
336
|
+
else await clearStop(current, item.threadKey);
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
if (publish && !storageFailed)
|
|
330
340
|
for (const item of [...value.own]) {
|
|
331
|
-
|
|
341
|
+
// Server consent alone cannot re-arm publication after a process restart:
|
|
342
|
+
// an OFF may have failed both durable storage and the network.
|
|
343
|
+
if (!item.enabled || pending.has(item.threadKey) || publicationGrants.get(item.threadKey) !== item.version) continue;
|
|
332
344
|
const candidate = candidates.find(
|
|
333
345
|
(c) => teamThreadKey(current, c) === item.threadKey,
|
|
334
346
|
);
|
|
@@ -384,9 +396,23 @@ export function createTeamThreads({
|
|
|
384
396
|
if (valid()) onChange();
|
|
385
397
|
});
|
|
386
398
|
},
|
|
387
|
-
async
|
|
399
|
+
async stopSharing(threadKey) {
|
|
400
|
+
scope();
|
|
401
|
+
if (!keyPattern.test(threadKey) || !value.own.some(row => row.threadKey === threadKey)) return false;
|
|
402
|
+
return setSharing(null, false, threadKey);
|
|
403
|
+
},
|
|
404
|
+
setSharing,
|
|
405
|
+
close() {
|
|
406
|
+
closed = true;
|
|
407
|
+
controller?.abort();
|
|
408
|
+
generation++;
|
|
409
|
+
publicationGrants.clear();
|
|
410
|
+
value = empty();
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
async function setSharing(candidate, enabled, existingKey) {
|
|
388
414
|
const current = scope(),
|
|
389
|
-
key = teamThreadKey(current, candidate),
|
|
415
|
+
key = existingKey ?? teamThreadKey(current, candidate),
|
|
390
416
|
item = value.own.find((row) => row.threadKey === key),
|
|
391
417
|
version = item?.version ?? 0;
|
|
392
418
|
if (
|
|
@@ -408,6 +434,7 @@ export function createTeamThreads({
|
|
|
408
434
|
storageFailed = false;
|
|
409
435
|
}
|
|
410
436
|
if (!enabled) {
|
|
437
|
+
publicationGrants.delete(key);
|
|
411
438
|
pending.set(key, version);
|
|
412
439
|
controller?.abort();
|
|
413
440
|
generation++;
|
|
@@ -442,16 +469,10 @@ export function createTeamThreads({
|
|
|
442
469
|
receipt.version !== version + 1
|
|
443
470
|
)
|
|
444
471
|
invalid();
|
|
472
|
+
if (valid() && !pending.has(key)) publicationGrants.set(key, receipt.version);
|
|
445
473
|
}
|
|
446
474
|
if (current.allowed) await context(send);
|
|
447
475
|
if (valid()) onChange();
|
|
448
476
|
}, !enabled);
|
|
449
|
-
|
|
450
|
-
close() {
|
|
451
|
-
closed = true;
|
|
452
|
-
controller?.abort();
|
|
453
|
-
generation++;
|
|
454
|
-
value = empty();
|
|
455
|
-
},
|
|
456
|
-
};
|
|
477
|
+
}
|
|
457
478
|
}
|