@aopslabs/aops 0.3.20 → 0.3.21

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.
Files changed (41) hide show
  1. package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/projectman.md +14 -0
  2. package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/manifest.json +5 -5
  3. package/agent-assets-release/agent-assets/inventory.json +5 -5
  4. package/agent-assets-release/agent-assets/projection.json +1 -1
  5. package/agent-assets-release/agent-assets-release.json +36 -36
  6. package/agent-assets-release/agent-assets-release.sigstore.json +28 -21
  7. package/assets/agent-assets/core/user-guides/projectman.md +14 -0
  8. package/dist/commands/chat-wake-watch.js +369 -0
  9. package/dist/commands/chatv3.js +74 -1
  10. package/dist/commands/codex.js +161 -0
  11. package/dist/commands/pm/index.js +3 -3
  12. package/dist/commands/pm/projectman.js +117 -20
  13. package/dist/lib/agent-assets/native-fs.js +2 -2
  14. package/dist/lib/cli-error-format.js +32 -0
  15. package/dist/lib/community-diagnostic.js +9 -2
  16. package/dist/main.js +9 -2
  17. package/native/manifest.json +1 -1
  18. package/native/qualifications/win32-x64.json +1 -1
  19. package/native/tui/darwin-arm64/aops-tui +0 -0
  20. package/native/tui/win32-x64/aops-tui.exe +0 -0
  21. package/package.json +7 -5
  22. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/SKILL.md +0 -0
  23. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/agentspace/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/aops-cli-core/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/chatv3/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/collaborative-work/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/discuss/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/docman/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/mission/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/projectman/SKILL.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/sys/SKILL.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/view/SKILL.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/references/working-disciplines/SKILL.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/agent-assets.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/agentspace.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/aops-cli.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/aops-system.md +0 -0
  38. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/chatv3.md +0 -0
  39. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/docman.md +0 -0
  40. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/sys.md +0 -0
  41. /package/agent-assets-release/agent-assets/core/{387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447 → 47516047e20f49d9df0f9c63f3ce98e96ee5b788baf13edf48613a2667edca3a}/files/user-guides/working-disciplines.md +0 -0
@@ -0,0 +1,369 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { Command } from 'commander';
7
+ import { wakeAgentSession } from '@aopslabs/aops-agent-runtime';
8
+ import { Chatv3Client } from '@aopslabs/domain-product-client-chatv3';
9
+ import { loadChatv3Session } from '../utils/chatv3-session-store.js';
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
+ const MEMBER_TOKEN = /^cv3m_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_[A-Za-z0-9_-]{16,}$/i;
12
+ const MAX_CAPTURE = 4 * 1024 * 1024;
13
+ const MEMBER_AUTH_REASONS = new Set([
14
+ 'member_token_required',
15
+ 'member_token_invalid_format',
16
+ 'member_token_unknown',
17
+ 'member_token_mismatch',
18
+ 'membership_inactive',
19
+ 'membership_channel_mismatch',
20
+ ]);
21
+ const RECOVERABLE_MEMBER_AUTH_REASONS = new Set([
22
+ 'member_token_unknown',
23
+ 'member_token_mismatch',
24
+ ]);
25
+ const record = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
26
+ const required = (value, label) => {
27
+ const result = value?.trim();
28
+ if (!result)
29
+ throw new Error(`${label} is required.`);
30
+ return result;
31
+ };
32
+ function integer(value, fallback, label, min, max) {
33
+ if (value === undefined)
34
+ return fallback;
35
+ const result = Number(value);
36
+ if (!Number.isSafeInteger(result) || result < min || result > max)
37
+ throw new Error(`${label} must be between ${min} and ${max}.`);
38
+ return result;
39
+ }
40
+ export function resolveWakeWatchConfig(options, env = process.env) {
41
+ if (options.wakeApproved !== true)
42
+ throw new Error('--wake-approved is required; watcher startup refused.');
43
+ const chatSession = required(options.session, '--session');
44
+ const room = options.room?.trim() || 'general';
45
+ const targetSessionId = required(options.targetSessionId, '--target-session-id');
46
+ if (!UUID.test(targetSessionId))
47
+ throw new Error('--target-session-id must be a UUID.');
48
+ const watcherMemberId = required(options.watcherMemberId, '--watcher-member-id');
49
+ const targetMemberId = required(options.targetMemberId, '--target-member-id');
50
+ if (!UUID.test(watcherMemberId))
51
+ throw new Error('--watcher-member-id must be a UUID.');
52
+ if (!UUID.test(targetMemberId))
53
+ throw new Error('--target-member-id must be a UUID.');
54
+ const ignoredMemberIds = new Set((options.ignoreMember ?? []).map((entry) => entry.trim()).filter(Boolean));
55
+ if (!ignoredMemberIds.has(watcherMemberId))
56
+ throw new Error('ignore-set must contain --watcher-member-id; startup refused.');
57
+ if (!ignoredMemberIds.has(targetMemberId))
58
+ throw new Error('ignore-set must contain --target-member-id; startup refused.');
59
+ const targetSessionHash = createHash('sha256').update(targetSessionId).digest('hex').slice(0, 24);
60
+ const stateName = createHash('sha256').update(`${chatSession}:${room}:${targetSessionHash}`).digest('hex').slice(0, 24);
61
+ return {
62
+ chatSession, room, targetSessionId, watcherMemberId, targetMemberId, ignoredMemberIds, targetSessionHash,
63
+ once: options.once === true,
64
+ initialCursor: integer(options.afterSeq, 0, '--after-seq', 0, Number.MAX_SAFE_INTEGER),
65
+ statePath: path.resolve(options.statePath ?? path.join(homedir(), '.aops', 'chat-wake-watch', `${stateName}.json`)),
66
+ storePath: options.storePath?.trim() || undefined,
67
+ sessionOwner: options.sessionOwner?.trim() || env.AOPS_CHATV3_SESSION_OWNER?.trim() || `wake-watch-${chatSession}`,
68
+ apiBaseUrl: options.apiBaseUrl?.trim() || env.AOPS_API_BASE_URL?.trim() || 'http://127.0.0.1:5900',
69
+ listenTimeoutSec: integer(options.listenTimeoutSec, 30, '--listen-timeout-sec', 1, 300),
70
+ intervalSec: integer(options.intervalSec, 2, '--interval-sec', 1, 60),
71
+ maxBackoffMs: integer(options.maxBackoffMs, 30_000, '--max-backoff-ms', 1_000, 300_000),
72
+ maxConsecutiveFailures: integer(options.maxConsecutiveFailures, 10, '--max-consecutive-failures', 1, 100),
73
+ };
74
+ }
75
+ const verifyWakeWatchMember = async ({ serverBaseUrl, channelId, memberToken }) => {
76
+ const client = new Chatv3Client({ serverBaseUrl, memberToken });
77
+ await client.listMembers(channelId, { status: 'active', limit: 1 });
78
+ };
79
+ export async function preflightWakeWatchSession(config, verifyMember = verifyWakeWatchMember) {
80
+ const loaded = await loadChatv3Session(config.chatSession, {
81
+ storePath: config.storePath,
82
+ owner: config.sessionOwner,
83
+ });
84
+ const channelId = loaded.record.channel.id?.trim();
85
+ if (!channelId || !UUID.test(channelId))
86
+ throw new Error('watcher ChatV3 session has an invalid channel id; startup refused.');
87
+ const parsed = MEMBER_TOKEN.exec(loaded.secrets.memberToken);
88
+ if (!parsed)
89
+ throw new Error('watcher ChatV3 session has an invalid member token; startup refused.');
90
+ if (parsed[1].toLowerCase() !== config.watcherMemberId.toLowerCase()) {
91
+ throw new Error('watcher member id does not match the owner-isolated ChatV3 session token; startup refused.');
92
+ }
93
+ try {
94
+ await verifyMember({
95
+ serverBaseUrl: config.apiBaseUrl || loaded.record.serverBaseUrl,
96
+ channelId,
97
+ memberToken: loaded.secrets.memberToken,
98
+ });
99
+ }
100
+ catch {
101
+ throw new Error('watcher ChatV3 member admission failed; startup refused.');
102
+ }
103
+ return { ...config, verifiedChannelId: channelId };
104
+ }
105
+ export function normalizeWakeWatchMessages(payload, cursor) {
106
+ const result = record(payload) && record(payload.result) ? payload.result : null;
107
+ if (!result || !Array.isArray(result.messages))
108
+ throw new Error('chat listen result.messages is not an array.');
109
+ return result.messages
110
+ .filter((message) => record(message) && Number.isSafeInteger(message.seq) && message.seq > cursor && typeof message.senderMemberId === 'string')
111
+ .sort((left, right) => left.seq - right.seq);
112
+ }
113
+ export const wakeWatchIdempotencyKey = (channelId, roomId, seq, hash) => `${channelId}:${roomId}:${seq}:${hash}`;
114
+ export function selectWakeWatchBatch(messages, config, state, channelId, roomId) {
115
+ const processed = new Set(state.processed);
116
+ const triggers = messages.filter((message) => !config.ignoredMemberIds.has(message.senderMemberId) && !processed.has(wakeWatchIdempotencyKey(channelId, roomId, message.seq, config.targetSessionHash)));
117
+ return { triggers, nextCursor: messages.at(-1)?.seq ?? state.cursor };
118
+ }
119
+ export function buildWakeWatchPrompt(config, channelId, roomId, triggers) {
120
+ const seqs = triggers.map((message) => message.seq);
121
+ const first = Math.min(...seqs);
122
+ const last = Math.max(...seqs);
123
+ return [
124
+ `AOPS ChatV3 channel=${channelId} room=${roomId} (${config.room}) icin yeni harici mesaj var.`,
125
+ `Tetikleyen seq araligi: ${first}-${last}; seq listesi: ${seqs.join(',')}.`,
126
+ `Mesaj icerigi wake bildirimine kopyalanmadi. Kendi runtime'ina ait kayitli ChatV3 kimliginle bu channel/room'u ${Math.max(0, first - 1)} cursor'undan sonra oku.`,
127
+ `Yerel AOPS API: ${config.apiBaseUrl}. Bu bildirim baska bir ajanin --session veya --session-owner degerini bilerek tasimaz.`,
128
+ 'Oda guidance kurallarina uy; dogrudan talimat veya soru varsa kisa ACK ver ve isi ele al.',
129
+ ].join('\n');
130
+ }
131
+ export function readWakeWatchState(statePath, initialCursor) {
132
+ try {
133
+ const value = JSON.parse(readFileSync(statePath, 'utf8'));
134
+ if (!record(value) || value.version !== 1 || !Number.isSafeInteger(value.cursor) || value.cursor < 0 || !Array.isArray(value.processed) || !value.processed.every((entry) => typeof entry === 'string'))
135
+ throw new Error('state contract invalid');
136
+ return { version: 1, cursor: value.cursor, processed: value.processed.slice(-1000), updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : null };
137
+ }
138
+ catch (error) {
139
+ if (error.code === 'ENOENT')
140
+ return { version: 1, cursor: initialCursor, processed: [], updatedAt: null };
141
+ throw new Error(`Cannot read wake-watch state: ${error instanceof Error ? error.message : String(error)}`);
142
+ }
143
+ }
144
+ export function writeWakeWatchState(statePath, state) {
145
+ mkdirSync(path.dirname(statePath), { recursive: true, mode: 0o700 });
146
+ const temporary = `${statePath}.${process.pid}.tmp`;
147
+ writeFileSync(temporary, `${JSON.stringify({ ...state, processed: state.processed.slice(-1000), updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
148
+ renameSync(temporary, statePath);
149
+ }
150
+ export const computeWakeWatchBackoff = (failures, maximum) => Math.min(1000 * 2 ** Math.min(Math.max(failures - 1, 0), 6), maximum);
151
+ const errorText = (error) => error instanceof Error ? error.message : String(error);
152
+ export function wakeWatchMemberAuthReason(error) {
153
+ if (!record(error) || !record(error.details))
154
+ return null;
155
+ const reason = error.details.memberAuthReason;
156
+ return typeof reason === 'string' && MEMBER_AUTH_REASONS.has(reason)
157
+ ? reason
158
+ : null;
159
+ }
160
+ export function isWakeWatchMemberCredentialError(error) {
161
+ const reason = wakeWatchMemberAuthReason(error);
162
+ return reason !== null && RECOVERABLE_MEMBER_AUTH_REASONS.has(reason);
163
+ }
164
+ export function isWakeWatchFatalIdentityError(error) {
165
+ const memberAuthReason = wakeWatchMemberAuthReason(error);
166
+ if (memberAuthReason)
167
+ return !RECOVERABLE_MEMBER_AUTH_REASONS.has(memberAuthReason);
168
+ const message = errorText(error);
169
+ return /(?:unknown|missing|invalid|not found|not active|removed|ended).{0,40}(?:chatv3 )?session|(?:chatv3 )?session.{0,40}(?:unknown|missing|invalid|not found)|channel.{0,40}(?:mismatch|not found)|room.{0,40}not found|member(?:ship)?.{0,40}(?:not found|not active|removed|ended)/i.test(message);
170
+ }
171
+ function classifiedMemberAuthError(message, reason) {
172
+ const error = new Error(message);
173
+ if (reason)
174
+ error.details = { memberAuthReason: reason };
175
+ return error;
176
+ }
177
+ function acquireLock(statePath) {
178
+ const lock = `${statePath}.lock`;
179
+ mkdirSync(path.dirname(lock), { recursive: true, mode: 0o700 });
180
+ for (let attempt = 0; attempt < 2; attempt += 1) {
181
+ try {
182
+ mkdirSync(lock, { mode: 0o700 });
183
+ writeFileSync(path.join(lock, 'pid'), `${process.pid}\n`, { mode: 0o600 });
184
+ return () => rmSync(lock, { recursive: true, force: true });
185
+ }
186
+ catch (error) {
187
+ if (error.code !== 'EEXIST')
188
+ throw error;
189
+ const pid = Number(readFileSync(path.join(lock, 'pid'), 'utf8').trim());
190
+ try {
191
+ process.kill(pid, 0);
192
+ throw new Error(`wake-watch already runs as PID ${pid}.`);
193
+ }
194
+ catch (probe) {
195
+ if (probe.code !== 'ESRCH')
196
+ throw probe;
197
+ rmSync(lock, { recursive: true, force: true });
198
+ }
199
+ }
200
+ }
201
+ throw new Error('Could not acquire wake-watch lock.');
202
+ }
203
+ export function parseWakeWatchNestedCliError(exitCode, stderr) {
204
+ const rendered = stderr.toString('utf8').trim();
205
+ try {
206
+ const payload = JSON.parse(rendered);
207
+ const structured = record(payload) && record(payload.error) ? payload.error : null;
208
+ if (structured && typeof structured.message === 'string') {
209
+ const error = new Error(structured.message);
210
+ if (typeof structured.errorCode === 'string')
211
+ error.errorCode = structured.errorCode;
212
+ if (record(structured.details))
213
+ error.details = structured.details;
214
+ return error;
215
+ }
216
+ }
217
+ catch {
218
+ }
219
+ return new Error(`nested ChatV3 exit ${exitCode}: ${rendered.split('\n').at(-1) ?? ''}`);
220
+ }
221
+ async function selfCli(args, accepted, timeoutMs) {
222
+ return new Promise((resolve, reject) => {
223
+ const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
224
+ let stdout = Buffer.alloc(0);
225
+ let stderr = Buffer.alloc(0);
226
+ let settled = false;
227
+ const finish = (error, value) => { if (settled)
228
+ return; settled = true; clearTimeout(timer); error ? reject(error) : resolve(value); };
229
+ const append = (current, chunk) => { const next = Buffer.concat([current, chunk]); if (next.length > MAX_CAPTURE) {
230
+ child.kill('SIGTERM');
231
+ finish(new Error('nested command output limit exceeded'));
232
+ } ; return next; };
233
+ child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk); });
234
+ child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk); });
235
+ child.once('error', (error) => finish(error));
236
+ child.once('close', (code) => {
237
+ if (!accepted.includes(code ?? -1))
238
+ return finish(parseWakeWatchNestedCliError(code, stderr));
239
+ try {
240
+ finish(undefined, { exitCode: code ?? 0, payload: JSON.parse(stdout.toString('utf8')) });
241
+ }
242
+ catch {
243
+ finish(new Error('nested ChatV3 returned invalid JSON'));
244
+ }
245
+ });
246
+ const timer = setTimeout(() => { child.kill('SIGTERM'); finish(new Error('nested ChatV3 timed out')); }, timeoutMs);
247
+ });
248
+ }
249
+ function common(config) { return [...(config.storePath ? ['--store-path', config.storePath] : []), '--session-owner', config.sessionOwner, '--api-base-url', config.apiBaseUrl, '--yes', '--json']; }
250
+ const log = (event, fields = {}) => process.stdout.write(`${JSON.stringify({ at: new Date().toISOString(), event, ...fields })}\n`);
251
+ const defaults = {
252
+ wake: wakeAgentSession,
253
+ listen: (config, cursor) => selfCli(['chat', 'listen', '--session', config.chatSession, '--room', config.room, '--after-seq', String(cursor), '--limit', '100', '--timeout-sec', String(config.listenTimeoutSec), '--interval-sec', String(config.intervalSec), ...common(config)], [0, 22], (config.listenTimeoutSec + 15) * 1000),
254
+ markRead: async (config, cursor, limit) => { await selfCli(['chat', 'read', '--session', config.chatSession, '--room', config.room, '--after-seq', String(cursor), '--limit', String(limit), '--mark-delivered', '--mark-read', ...common(config)], [0], 30_000); },
255
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
256
+ };
257
+ export async function runWakeWatch(config, dependencies = defaults) {
258
+ const probe = await dependencies.wake({ runtime: 'codex', transport: 'desktop-ipc', sessionId: config.targetSessionId, prompt: 'AOPS wake-watch startup dry-run probe.', dryRun: true });
259
+ if (!probe.envelope.ok)
260
+ throw new Error(`startup dry-run probe failed: ${probe.envelope.code}`);
261
+ const release = acquireLock(config.statePath);
262
+ let stopping = false;
263
+ let failures = 0;
264
+ let credentialRetryPending = false;
265
+ const stop = () => { stopping = true; };
266
+ process.once('SIGINT', stop);
267
+ process.once('SIGTERM', stop);
268
+ try {
269
+ let state = readWakeWatchState(config.statePath, config.initialCursor);
270
+ log('wake_watch_started', { cursor: state.cursor, room: config.room, targetSessionHash: config.targetSessionHash, mode: config.once ? 'once' : 'watch' });
271
+ while (!stopping) {
272
+ try {
273
+ const previousCursor = state.cursor;
274
+ const listened = await dependencies.listen(config, state.cursor);
275
+ const messages = normalizeWakeWatchMessages(listened.payload, state.cursor);
276
+ if (listened.exitCode === 22 || messages.length === 0) {
277
+ failures = 0;
278
+ credentialRetryPending = false;
279
+ if (config.once)
280
+ break;
281
+ continue;
282
+ }
283
+ const artifacts = record(listened.payload) && record(listened.payload.artifacts) ? listened.payload.artifacts : {};
284
+ const channelId = typeof artifacts.channelId === 'string' ? artifacts.channelId : '';
285
+ const roomId = typeof artifacts.roomId === 'string' ? artifacts.roomId : '';
286
+ if (!channelId || !roomId)
287
+ throw new Error('chat listen artifacts missing channelId/roomId');
288
+ if (config.verifiedChannelId && channelId !== config.verifiedChannelId) {
289
+ throw new Error('fatal ChatV3 identity error: listen channel does not match the owner-isolated watcher session');
290
+ }
291
+ const selected = selectWakeWatchBatch(messages, config, state, channelId, roomId);
292
+ if (selected.triggers.length) {
293
+ const wake = await dependencies.wake({ runtime: 'codex', transport: 'desktop-ipc', sessionId: config.targetSessionId, prompt: buildWakeWatchPrompt(config, channelId, roomId, selected.triggers) });
294
+ if (!wake.envelope.ok)
295
+ throw new Error(`wake failed: ${wake.envelope.code}`);
296
+ const keys = selected.triggers.map((message) => wakeWatchIdempotencyKey(channelId, roomId, message.seq, config.targetSessionHash));
297
+ state = { ...state, cursor: selected.nextCursor, processed: [...state.processed, ...keys].slice(-1000) };
298
+ log('wake_sent', { seq: selected.triggers.map((message) => message.seq) });
299
+ }
300
+ else {
301
+ state = { ...state, cursor: selected.nextCursor };
302
+ log('ignored_batch', { seq: messages.map((message) => message.seq) });
303
+ }
304
+ writeWakeWatchState(config.statePath, state);
305
+ try {
306
+ await dependencies.markRead(config, previousCursor, messages.length);
307
+ }
308
+ catch (error) {
309
+ log('receipt_warning', { error: error instanceof Error ? error.message : String(error) });
310
+ }
311
+ failures = 0;
312
+ credentialRetryPending = false;
313
+ if (config.once)
314
+ break;
315
+ }
316
+ catch (error) {
317
+ const message = errorText(error);
318
+ const memberAuthReason = wakeWatchMemberAuthReason(error);
319
+ if (isWakeWatchFatalIdentityError(error)) {
320
+ throw classifiedMemberAuthError(`fatal ChatV3 identity error: ${message}`, memberAuthReason);
321
+ }
322
+ if (isWakeWatchMemberCredentialError(error)) {
323
+ if (credentialRetryPending) {
324
+ throw classifiedMemberAuthError(`member credential remained invalid after one automatic recovery attempt: ${message}`, memberAuthReason);
325
+ }
326
+ credentialRetryPending = true;
327
+ log('wake_watch_credential_recovery', {
328
+ action: 'reload_saved_session_and_retry_once',
329
+ memberAuthReason,
330
+ });
331
+ continue;
332
+ }
333
+ if (config.once)
334
+ throw error;
335
+ failures += 1;
336
+ if (failures >= config.maxConsecutiveFailures) {
337
+ throw new Error(`maximum consecutive failures reached (${config.maxConsecutiveFailures}): ${message}`);
338
+ }
339
+ const backoffMs = computeWakeWatchBackoff(failures, config.maxBackoffMs);
340
+ log('wake_watch_error', { error: message, backoffMs });
341
+ await (dependencies.sleep ?? defaults.sleep)(backoffMs);
342
+ }
343
+ }
344
+ }
345
+ finally {
346
+ process.off('SIGINT', stop);
347
+ process.off('SIGTERM', stop);
348
+ release();
349
+ log('wake_watch_stopped');
350
+ }
351
+ }
352
+ export function makeChatWakeWatchCommand() {
353
+ return new Command('wake-watch').description('EXPERIMENTAL foreground ChatV3-to-local-Codex wake watcher')
354
+ .requiredOption('--session <id>', 'Watcher-owned local ChatV3 session').option('--room <slugOrId>', 'Room slug or id', 'general')
355
+ .requiredOption('--target-session-id <uuid>', 'Exact local Codex task UUID').requiredOption('--watcher-member-id <uuid>', 'Watcher ChatV3 member id')
356
+ .requiredOption('--target-member-id <uuid>', 'Target agent ChatV3 member id').option('--ignore-member <uuid>', 'Repeatable explicit ignore-set member id', (value, previous) => [...previous, value], [])
357
+ .requiredOption('--wake-approved', 'Confirm operator approval for local session wake').option('--once', 'Run one bounded listen cycle').option('--after-seq <seq>', 'Initial processing cursor', '0')
358
+ .option('--state-path <path>', 'Processing-state override').option('--store-path <path>', 'ChatV3 session store override').option('--session-owner <id>', 'Watcher session owner')
359
+ .option('--api-base-url <url>', 'AOPS API base URL').option('--listen-timeout-sec <n>', 'Listen timeout', '30').option('--interval-sec <n>', 'Poll interval', '2').option('--max-backoff-ms <n>', 'Maximum retry backoff', '30000').option('--max-consecutive-failures <n>', 'Stop after this many transient failures', '10')
360
+ .option('--yes', 'Non-interactive').option('--json', 'JSON-lines status').action(async (options) => { try {
361
+ const config = await preflightWakeWatchSession(resolveWakeWatchConfig(options));
362
+ await runWakeWatch(config);
363
+ }
364
+ catch (error) {
365
+ log('wake_watch_fatal', { error: error instanceof Error ? error.message : String(error), memberAuthReason: wakeWatchMemberAuthReason(error) });
366
+ process.exitCode = 1;
367
+ } })
368
+ .addHelpText('after', '\nEXPERIMENTAL / UNSUPPORTED\n Chat-triggered wake requires this watcher\'s owner-isolated active channel member token; the token and member id must match before IPC probing or listen begins.\n Uses version-sensitive Codex Desktop IPC. Startup fails closed unless the ignore-set is complete and dry-run owner probing succeeds.\n Message text, the target Codex session id, and another agent\'s ChatV3 session/owner identity are never copied into ChatV3 coordination.\n This is a normal-flow identity boundary, not isolation from another local process running as the same OS user.\n');
369
+ }
@@ -6,7 +6,26 @@ import { resolveCliApiBaseUrl } from '../utils/api.js';
6
6
  import { planChatv3SendCursorAdvance } from '../utils/chatv3-cursor.js';
7
7
  import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
8
8
  import { buildOperatorCookbook } from '../utils/hosted-sugar.js';
9
+ import { makeChatWakeWatchCommand } from './chat-wake-watch.js';
9
10
  import { deleteChatv3Session, getChatv3SessionRecord, listChatv3SessionRecords, loadChatv3Session, resolveChatv3SessionStorePath, resolveChatv3SessionOwner, saveChatv3Session, summarizeChatv3Session, } from '../utils/chatv3-session-store.js';
11
+ const CHATV3_MEMBER_TOKEN_ID = /^cv3m_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})_/i;
12
+ export function memberIdFromChatv3Token(memberToken) {
13
+ return CHATV3_MEMBER_TOKEN_ID.exec(memberToken)?.[1] ?? null;
14
+ }
15
+ export function isChatv3MemberCredentialError(error) {
16
+ const message = error instanceof Error ? error.message : String(error);
17
+ return /(member token (?:mismatch|required|unknown|not found)|unknown member token|invalid member token(?: format)?|membership is not active)/i.test(message);
18
+ }
19
+ export class Chatv3IdentityRecoveryError extends Error {
20
+ code = 'chatv3_identity_recovery_failed';
21
+ constructor(sessionId, cause) {
22
+ const detail = cause instanceof Error ? cause.message : String(cause);
23
+ super(`Automatic exact-member token recovery failed for ChatV3 session "${sessionId}": ${detail}`, {
24
+ cause: cause instanceof Error ? cause : undefined,
25
+ });
26
+ this.name = 'Chatv3IdentityRecoveryError';
27
+ }
28
+ }
10
29
  const AGENT_BOOTSTRAP_STEPS = [
11
30
  'Read channel and active-room rules before acting.',
12
31
  'Use chat for coordination and wake; use PM for task/review truth.',
@@ -26,6 +45,7 @@ const CHATV3_COMMAND_TREE = {
26
45
  send: null,
27
46
  read: null,
28
47
  listen: null,
48
+ 'wake-watch': null,
29
49
  leave: null,
30
50
  binding: new Set(['add', 'list', 'remove']),
31
51
  member: new Set(['list', 'remove', 'restore']),
@@ -510,7 +530,7 @@ async function persistRuntimeSnapshot(runtime, activeRoomId) {
510
530
  owner: runtime.sessionOwner,
511
531
  });
512
532
  }
513
- async function loadRuntime(options, sessionId) {
533
+ async function loadRuntimeAttempt(options, sessionId) {
514
534
  const storePath = resolveChatv3SessionStorePath(options.storePath, options.sessionOwner);
515
535
  const loaded = await loadChatv3Session(sessionId, {
516
536
  storePath: options.storePath,
@@ -566,6 +586,58 @@ async function loadRuntime(options, sessionId) {
566
586
  await persistRuntimeSnapshot(runtime);
567
587
  return runtime;
568
588
  }
589
+ function plainSessionInput(loaded) {
590
+ return {
591
+ id: loaded.record.id,
592
+ serverBaseUrl: loaded.record.serverBaseUrl,
593
+ handle: loaded.record.handle,
594
+ channel: loaded.record.channel,
595
+ keyId: loaded.record.keyId,
596
+ activeRoomId: loaded.record.activeRoomId,
597
+ rooms: loaded.record.rooms,
598
+ createdAt: loaded.record.createdAt,
599
+ };
600
+ }
601
+ async function recoverStoredMemberToken(options, sessionId) {
602
+ const loaded = await loadChatv3Session(sessionId, {
603
+ storePath: options.storePath,
604
+ owner: options.sessionOwner,
605
+ });
606
+ if (loaded.record.channel.encryptionMode !== 'server-encrypted') {
607
+ throw new Error('exact-member token recovery is available only for server-encrypted channels');
608
+ }
609
+ const memberId = memberIdFromChatv3Token(loaded.secrets.memberToken);
610
+ if (!memberId)
611
+ throw new Error('stored member token does not contain a recoverable member identity');
612
+ const serverBaseUrl = normalizeNonEmpty(options.apiBaseUrl) ?? loaded.record.serverBaseUrl;
613
+ const client = new Chatv3Client({ serverBaseUrl });
614
+ const recovered = await client.http.post(`/channels/${loaded.record.channel.id}/members/${memberId}/token`, {
615
+ tenantId: loaded.record.channel.tenantId,
616
+ channelId: loaded.record.channel.id,
617
+ memberId,
618
+ });
619
+ if (recovered.member.id !== memberId || memberIdFromChatv3Token(recovered.memberToken) !== memberId) {
620
+ throw new Error('server returned a token for a different member identity');
621
+ }
622
+ await saveChatv3Session(plainSessionInput(loaded), { ...loaded.secrets, memberToken: recovered.memberToken }, { storePath: options.storePath, owner: options.sessionOwner });
623
+ process.stderr.write(`[chatv3] Recovered the exact member credential for session "${sessionId}".\n`);
624
+ }
625
+ async function loadRuntime(options, sessionId) {
626
+ try {
627
+ return await loadRuntimeAttempt(options, sessionId);
628
+ }
629
+ catch (error) {
630
+ if (!isChatv3MemberCredentialError(error))
631
+ throw error;
632
+ try {
633
+ await recoverStoredMemberToken(options, sessionId);
634
+ return await loadRuntimeAttempt(options, sessionId);
635
+ }
636
+ catch (recoveryError) {
637
+ throw new Chatv3IdentityRecoveryError(sessionId, recoveryError);
638
+ }
639
+ }
640
+ }
569
641
  function channelMatchesSelector(channel, selector) {
570
642
  return channel.id === selector || channel.slug === selector || (selector.length >= 8 && channel.id.startsWith(selector));
571
643
  }
@@ -1837,6 +1909,7 @@ export function makeChatv3Command(commandName = 'chat') {
1837
1909
  .option('--mark-read', 'Advance read cursor to the highest returned seq')).action(async (options) => {
1838
1910
  await runChatv3Listen(options);
1839
1911
  });
1912
+ cmd.addCommand(makeChatWakeWatchCommand());
1840
1913
  const binding = cmd.command('binding').description('ChatV3 loose channel/room reference bindings');
1841
1914
  applyChatv3Options(binding.command('add')
1842
1915
  .description('Attach a loose external reference to the active room or channel')
@@ -0,0 +1,161 @@
1
+ import { AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES, listAgentWakeTargets, registerAgentWakeTarget, resolveAgentWakeRegistryPath, unregisterAgentWakeTarget, wakeAgentSession, } from '@aopslabs/aops-agent-runtime';
2
+ import { Command } from 'commander';
3
+ async function readBoundedStdin() {
4
+ const chunks = [];
5
+ let bytes = 0;
6
+ for await (const chunk of process.stdin) {
7
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), 'utf8');
8
+ bytes += buffer.length;
9
+ if (bytes > AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES) {
10
+ throw new Error(`Prompt exceeds ${AGENT_RUNTIME_WAKE_MAX_PROMPT_BYTES} UTF-8 bytes.`);
11
+ }
12
+ chunks.push(buffer);
13
+ }
14
+ return Buffer.concat(chunks).toString('utf8');
15
+ }
16
+ function failureEnvelope(options, code, message) {
17
+ return {
18
+ ok: false,
19
+ code,
20
+ transport: options.transport ?? 'desktop-ipc',
21
+ runtime: 'codex',
22
+ sessionId: options.sessionId ?? '',
23
+ ownerClientId: null,
24
+ turnId: null,
25
+ protocolVersion: null,
26
+ protocolVersionVerified: false,
27
+ runtimeVersion: null,
28
+ runtimeVersionSupported: false,
29
+ stage: 'validation',
30
+ message,
31
+ retryable: false,
32
+ };
33
+ }
34
+ async function runCodexWake(options) {
35
+ let prompt = options.prompt ?? '';
36
+ try {
37
+ if (prompt === '-')
38
+ prompt = await readBoundedStdin();
39
+ }
40
+ catch (error) {
41
+ process.stdout.write(`${JSON.stringify(failureEnvelope(options, 'prompt_read_failed', error instanceof Error ? error.message : String(error)))}\n`);
42
+ process.exitCode = 2;
43
+ return;
44
+ }
45
+ const result = await wakeAgentSession({
46
+ runtime: 'codex',
47
+ sessionId: options.sessionId ?? '',
48
+ prompt,
49
+ transport: options.transport ?? 'desktop-ipc',
50
+ dryRun: options.dryRun === true,
51
+ });
52
+ process.stdout.write(`${JSON.stringify(result.envelope)}\n`);
53
+ if (result.exitCode !== 0)
54
+ process.exitCode = result.exitCode;
55
+ }
56
+ function writeRegistryResult(payload, exitCode = 0) {
57
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
58
+ if (exitCode !== 0)
59
+ process.exitCode = exitCode;
60
+ }
61
+ async function runCodexRegister(options) {
62
+ const sessionId = options.sessionId ?? '';
63
+ const probe = await wakeAgentSession({ runtime: 'codex', transport: 'desktop-ipc', sessionId, prompt: '', dryRun: true });
64
+ if (!probe.envelope.ok) {
65
+ writeRegistryResult(probe.envelope, probe.exitCode);
66
+ return;
67
+ }
68
+ try {
69
+ const entry = registerAgentWakeTarget({
70
+ channelId: options.channelId ?? '',
71
+ memberId: options.memberId ?? '',
72
+ handle: options.handle,
73
+ targetSessionId: sessionId,
74
+ registryPath: options.registryPath,
75
+ });
76
+ writeRegistryResult({
77
+ ok: true,
78
+ code: 'agent_wake_target_registered',
79
+ runtime: entry.runtime,
80
+ channelId: entry.channelId,
81
+ memberId: entry.memberId,
82
+ handle: entry.handle,
83
+ targetSessionIdHash: entry.targetSessionIdHash,
84
+ hostMatched: true,
85
+ lastVerifiedAt: entry.lastVerifiedAt,
86
+ });
87
+ }
88
+ catch (error) {
89
+ writeRegistryResult({ ok: false, code: 'agent_wake_registry_write_failed', message: error instanceof Error ? error.message : String(error) }, 1);
90
+ }
91
+ }
92
+ function runCodexRegistrations(options) {
93
+ try {
94
+ const entries = listAgentWakeTargets(options.registryPath).map((entry) => ({
95
+ runtime: entry.runtime,
96
+ channelId: entry.channelId,
97
+ memberId: entry.memberId,
98
+ handle: entry.handle,
99
+ targetSessionIdHash: entry.targetSessionIdHash,
100
+ lastVerifiedAt: entry.lastVerifiedAt,
101
+ }));
102
+ writeRegistryResult({ ok: true, code: 'agent_wake_registrations', registryPath: resolveAgentWakeRegistryPath(options.registryPath), entries });
103
+ }
104
+ catch (error) {
105
+ writeRegistryResult({ ok: false, code: 'agent_wake_registry_read_failed', message: error instanceof Error ? error.message : String(error) }, 1);
106
+ }
107
+ }
108
+ function runCodexUnregister(options) {
109
+ try {
110
+ const removed = unregisterAgentWakeTarget(options.channelId ?? '', options.memberId ?? '', options.registryPath);
111
+ writeRegistryResult({ ok: true, code: removed ? 'agent_wake_target_unregistered' : 'agent_wake_target_not_found', channelId: options.channelId, memberId: options.memberId });
112
+ }
113
+ catch (error) {
114
+ writeRegistryResult({ ok: false, code: 'agent_wake_registry_write_failed', message: error instanceof Error ? error.message : String(error) }, 1);
115
+ }
116
+ }
117
+ export function makeCodexCommand() {
118
+ const command = new Command('codex')
119
+ .description('Experimental local agent-runtime integrations for Codex Desktop');
120
+ command
121
+ .command('wake')
122
+ .alias('wakeup')
123
+ .description('Wake the exact local Codex Desktop session through internal IPC')
124
+ .option('--session-id <uuid>', 'Exact Codex Desktop task/session UUID')
125
+ .option('--prompt <text-or-dash>', 'Bounded prompt text, or - to read it from stdin')
126
+ .option('--transport <transport>', 'Transport (only desktop-ipc is supported)', 'desktop-ipc')
127
+ .option('--dry-run', 'Probe the exact local owner without sending a turn')
128
+ .action(runCodexWake)
129
+ .addHelpText('after', `
130
+ EXPERIMENTAL / UNSUPPORTED
131
+ desktop-ipc is an internal, version-sensitive Codex Desktop protocol.
132
+ This command fails closed and never falls back to the public SDK or app-server.
133
+ A successful non-dry-run injects the bounded prompt into the selected local task.
134
+ This low-level local IPC primitive is not a ChatV3 authorization surface.
135
+ Chat-triggered wake must first prove the caller's owner-isolated active channel member token in the watcher or Cockpit flow.
136
+ These checks prevent normal-flow member substitution; they do not isolate another process running as the same OS user.
137
+ `);
138
+ command
139
+ .command('register')
140
+ .description('Register a verified local Codex task for one ChatV3 agent member')
141
+ .requiredOption('--channel-id <uuid>', 'Exact ChatV3 channel UUID')
142
+ .requiredOption('--member-id <uuid>', 'Exact ChatV3 agent member UUID')
143
+ .option('--handle <handle>', 'Expected ChatV3 handle for identity binding')
144
+ .requiredOption('--session-id <uuid>', 'Exact local Codex task/session UUID')
145
+ .option('--registry-path <path>', 'Local registry override for named-instance testing')
146
+ .action(runCodexRegister)
147
+ .addHelpText('after', '\nEXPERIMENTAL / UNSUPPORTED\n Registration writes only after a live fail-closed Desktop owner dry-run succeeds.\n The registry is local, atomic, owner-only (0600 on POSIX), and host-bound.\n');
148
+ command
149
+ .command('registrations')
150
+ .description('List local ChatV3-to-Codex registrations with session ids redacted')
151
+ .option('--registry-path <path>', 'Local registry override')
152
+ .action(runCodexRegistrations);
153
+ command
154
+ .command('unregister')
155
+ .description('Remove one local ChatV3 member wake registration')
156
+ .requiredOption('--channel-id <uuid>', 'Exact ChatV3 channel UUID')
157
+ .requiredOption('--member-id <uuid>', 'Exact ChatV3 agent member UUID')
158
+ .option('--registry-path <path>', 'Local registry override')
159
+ .action(runCodexUnregister);
160
+ return command;
161
+ }