@supen-ai/cli 1.4.8 → 1.4.9

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.
@@ -6,7 +6,7 @@ import { createRequire } from 'module';
6
6
  import { spawn, spawnSync } from 'child_process';
7
7
  import { DAEMON_HOSTNAME, DEFAULT_MODEL, MODELS_REGISTRY, SUPEN_HOME, reloadModelsRegistry, } from '../../core/config.js';
8
8
  import { readConfigSummary, readConfigYamlFile, writeConfigYamlFile, } from '../../core/env.js';
9
- import { readSpaceEnvMap, updateSpaceEnvMap, withRuntimeSpaceEnv, withSupenLlmEnv } from '../../core/space-env.js';
9
+ import { readSpaceEnvMap, updateSpaceEnvMap, withSupenLlmEnv } from '../../core/space-env.js';
10
10
  import { readCodexSubscription } from '../../core/codex-subscription.js';
11
11
  import { buildHubSnapshotForSpace } from '../../core/hub-snapshot.js';
12
12
  import { readEnrollmentState, createEnrollmentToken, verifyEnrollmentToken, setTrustState } from '../../core/enrollment.js';
@@ -22,10 +22,10 @@ import { getMcpManager } from '../../mcp/index.js';
22
22
  import { logger } from '../../core/logger.js';
23
23
  import { buildDaemonOpenApiSpec } from '../../channels/http-routes.js';
24
24
  import { writeJson, writeProtocolError, readJsonBody } from '../response.js';
25
- import { listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
25
+ import { appendThreadEvent, listRecentThreadEventsAfter, readThreadEventLogHead, } from '../../core/thread-event-log.js';
26
26
  import { projectThreadRuntimeState } from '../../core/thread-runtime-state.js';
27
27
  import { attachThreadContextToChunk, readThreadContext, } from '../../core/thread-context.js';
28
- import { addThreadStreamClient, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
28
+ import { addThreadStreamClient, broadcastToThread, removeThreadStreamClient, writeThreadStreamEvent } from '../thread-stream.js';
29
29
  const DEFAULT_TOKEN_TTL_MINUTES = 20;
30
30
  const SPACE_LOG_EVENT_LIMIT = 200;
31
31
  const SPACE_LOG_STREAM_POLL_MS = 1000;
@@ -882,141 +882,6 @@ function readThreadWorkspacePath(filePath) {
882
882
  }
883
883
  return null;
884
884
  }
885
- const CODEX_THREAD_OBSERVER_STDOUT_MAX_BYTES = 4 * 1024 * 1024;
886
- function startCodexThreadObserver(input) {
887
- const codexPath = resolveCliExecutablePath('codex');
888
- if (!codexPath) {
889
- input.write({
890
- type: 'error',
891
- message: 'codex executable was not found in the daemon tool lookup PATH',
892
- });
893
- return () => { };
894
- }
895
- const child = spawn(codexPath, ['app-server', '--listen', 'stdio://'], {
896
- cwd: input.cwd,
897
- stdio: ['pipe', 'pipe', 'pipe'],
898
- env: withRuntimeSpaceEnv(buildToolLookupEnv()),
899
- });
900
- let buffer = '';
901
- let nextId = 1;
902
- let observerEventIndex = 0;
903
- let initialized = false;
904
- let closed = false;
905
- child.on('error', (err) => {
906
- if (closed)
907
- return;
908
- input.write({
909
- type: 'error',
910
- message: err instanceof Error ? err.message : String(err),
911
- });
912
- });
913
- child.on('close', () => {
914
- closed = true;
915
- });
916
- child.stdin.on('error', (error) => {
917
- closed = true;
918
- if (error?.code === 'EPIPE' || error?.code === 'ECONNRESET')
919
- return;
920
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer stdin failed');
921
- });
922
- const closeObserver = (signal = 'SIGTERM') => {
923
- if (closed)
924
- return;
925
- closed = true;
926
- try {
927
- child.stdin.destroy();
928
- child.kill(signal);
929
- }
930
- catch {
931
- // Observer cleanup is best-effort; the HTTP stream is already closing.
932
- }
933
- };
934
- const send = (message) => {
935
- if (closed || child.stdin.destroyed || !child.stdin.writable)
936
- return;
937
- try {
938
- child.stdin.write(`${JSON.stringify(message)}\n`);
939
- }
940
- catch (error) {
941
- closed = true;
942
- const code = error?.code;
943
- if (code === 'EPIPE' || code === 'ECONNRESET')
944
- return;
945
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer write failed');
946
- }
947
- };
948
- const request = (method, params) => {
949
- send({ id: nextId++, method, ...(params ? { params } : {}) });
950
- };
951
- child.stdout.on('data', (chunk) => {
952
- if (closed)
953
- return;
954
- buffer += String(chunk);
955
- if (buffer.length > CODEX_THREAD_OBSERVER_STDOUT_MAX_BYTES) {
956
- logger.debug({ threadId: input.threadId, bytes: buffer.length }, 'Codex thread observer stdout exceeded line buffer limit');
957
- closeObserver('SIGKILL');
958
- return;
959
- }
960
- while (true) {
961
- const newlineIndex = buffer.indexOf('\n');
962
- if (newlineIndex < 0)
963
- break;
964
- const line = buffer.slice(0, newlineIndex).trim();
965
- buffer = buffer.slice(newlineIndex + 1);
966
- if (!line.startsWith('{'))
967
- continue;
968
- let parsed;
969
- try {
970
- parsed = JSON.parse(line);
971
- }
972
- catch {
973
- continue;
974
- }
975
- if (parsed.id !== undefined && typeof parsed.method === 'string') {
976
- send({ id: parsed.id, result: {} });
977
- continue;
978
- }
979
- if (parsed.id !== undefined) {
980
- if (!initialized && parsed.id === 1) {
981
- initialized = true;
982
- send({ method: 'initialized' });
983
- request('thread/resume', {
984
- threadId: input.threadId,
985
- cwd: input.cwd,
986
- experimentalRawEvents: true,
987
- });
988
- }
989
- continue;
990
- }
991
- const method = typeof parsed.method === 'string' ? parsed.method : '';
992
- if (!method)
993
- continue;
994
- const params = parsed.params && typeof parsed.params === 'object'
995
- ? parsed.params
996
- : {};
997
- const eventThreadId = typeof params.threadId === 'string' ? params.threadId : '';
998
- if (eventThreadId !== input.threadId)
999
- continue;
1000
- observerEventIndex += 1;
1001
- input.write(codexAppServerEventChunk(method, params), `observer-${Date.now()}-${observerEventIndex}`);
1002
- }
1003
- });
1004
- child.stderr.on('data', () => { });
1005
- child.once('error', (error) => {
1006
- logger.debug({ err: error, threadId: input.threadId }, 'Codex thread observer failed');
1007
- });
1008
- request('initialize', {
1009
- clientInfo: {
1010
- name: 'supen-daemon-thread-observer',
1011
- title: 'Supen',
1012
- version: '0.1.0',
1013
- },
1014
- capabilities: { experimentalApi: true },
1015
- });
1016
- return () => {
1017
- closeObserver();
1018
- };
1019
- }
1020
885
  function normalizeThreadStatusToken(value) {
1021
886
  return typeof value === 'string'
1022
887
  ? value.trim().toLowerCase().replace(/[-_\s]/g, '')
@@ -1465,6 +1330,23 @@ function isMirroredGoalContextMessage(text) {
1465
1330
  const trimmed = text.trim();
1466
1331
  return trimmed.startsWith('<goal_context>') && trimmed.endsWith('</goal_context>');
1467
1332
  }
1333
+ function threadGoalEventAction(event) {
1334
+ const raw = event.raw_payload && typeof event.raw_payload === 'object'
1335
+ ? event.raw_payload
1336
+ : {};
1337
+ const method = typeof raw.method === 'string' ? raw.method : '';
1338
+ const params = raw.params && typeof raw.params === 'object' ? raw.params : {};
1339
+ if (method === 'thread/goal/cleared')
1340
+ return 'cleared';
1341
+ if (method !== 'thread/goal/updated')
1342
+ return null;
1343
+ const status = typeof params.status === 'string' ? params.status.trim().toLowerCase() : '';
1344
+ if (status === 'paused')
1345
+ return 'paused';
1346
+ if (status === 'active' || status === 'resumed')
1347
+ return 'active';
1348
+ return null;
1349
+ }
1468
1350
  function mirroredThreadWorkspacePath(threadPath, stateEntry) {
1469
1351
  const stateCwd = typeof stateEntry?.cwd === 'string' ? stateEntry.cwd : null;
1470
1352
  const threadWorkspace = readThreadWorkspacePath(threadPath);
@@ -1737,6 +1619,22 @@ export function readCodexThreadHistory(threadId, limit = MIRRORED_THREAD_HISTORY
1737
1619
  chunk,
1738
1620
  });
1739
1621
  }
1622
+ for (const event of usableEventLogEvents) {
1623
+ const action = threadGoalEventAction(event);
1624
+ if (!action)
1625
+ continue;
1626
+ const eventTimestampMs = Date.parse(event.received_at);
1627
+ const goalTimestampMs = recentGoalContext ? Date.parse(recentGoalContext.timestamp) : 0;
1628
+ if (recentGoalContext && Number.isFinite(eventTimestampMs) && Number.isFinite(goalTimestampMs) && eventTimestampMs < goalTimestampMs) {
1629
+ continue;
1630
+ }
1631
+ if (action === 'cleared') {
1632
+ recentGoalContext = null;
1633
+ }
1634
+ else if (recentGoalContext) {
1635
+ recentGoalContext = { ...recentGoalContext, status: action };
1636
+ }
1637
+ }
1740
1638
  const collapsedMessages = collapseCompletedAssistantProgressMessages(messages);
1741
1639
  const slicedMessages = collapsedMessages.slice(-safeLimit);
1742
1640
  const slicedEvents = events.slice(-safeLimit * 8);
@@ -3202,7 +3100,6 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3202
3100
  const workspacePath = readThreadWorkspacePath(threadPath) || process.cwd();
3203
3101
  const withThreadContext = (chunk) => attachThreadContextToChunk(chunk, readThreadContext({ threadId, workspace: workspacePath }));
3204
3102
  let streamClosed = false;
3205
- let stopCodexObserver = () => { };
3206
3103
  let pingTimer = null;
3207
3104
  let jsonlPollTimer = null;
3208
3105
  const cleanup = () => {
@@ -3213,22 +3110,8 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3213
3110
  clearInterval(pingTimer);
3214
3111
  if (jsonlPollTimer)
3215
3112
  clearInterval(jsonlPollTimer);
3216
- stopCodexObserver();
3217
3113
  removeThreadStreamClient(threadId, res);
3218
3114
  };
3219
- stopCodexObserver = process.env.NODE_ENV === 'test'
3220
- ? () => { }
3221
- : startCodexThreadObserver({
3222
- threadId,
3223
- cwd: workspacePath,
3224
- write: (chunk, id) => {
3225
- if (streamClosed)
3226
- return;
3227
- const wrote = writeThreadStreamEvent(res, withThreadContext(chunk), id ? { id } : {});
3228
- if (!wrote)
3229
- cleanup();
3230
- },
3231
- });
3232
3115
  pingTimer = setInterval(() => {
3233
3116
  if (!writeSse(res)) {
3234
3117
  cleanup();
@@ -3323,6 +3206,52 @@ export async function handleSystemRoutes(req, res, url, pathname, method) {
3323
3206
  writeJson(res, 200, { thread: serializeAdoptedMirroredThread(thread) });
3324
3207
  return true;
3325
3208
  }
3209
+ const mirroredThreadGoalMatch = codexThreadActionRoute(pathname, 'goal');
3210
+ if (mirroredThreadGoalMatch && (method === 'PATCH' || method === 'DELETE')) {
3211
+ const threadId = decodeURIComponent(mirroredThreadGoalMatch[1] || '').trim();
3212
+ if (!threadId || !codexThreadFiles().has(threadId)) {
3213
+ writeProtocolError(res, 404, 'not_found', 'mirrored_thread_not_found', 'Mirrored task history was not found.');
3214
+ return true;
3215
+ }
3216
+ let status = null;
3217
+ if (method === 'PATCH') {
3218
+ const parsed = await readJsonBody(req);
3219
+ const rawStatus = parsed && typeof parsed === 'object' && typeof parsed.status === 'string'
3220
+ ? String(parsed.status).trim().toLowerCase()
3221
+ : '';
3222
+ if (rawStatus === 'active' || rawStatus === 'resumed')
3223
+ status = 'active';
3224
+ if (rawStatus === 'paused')
3225
+ status = 'paused';
3226
+ if (!status) {
3227
+ writeProtocolError(res, 400, 'validation_error', 'invalid_goal_status', 'Expected goal status "active" or "paused".');
3228
+ return true;
3229
+ }
3230
+ }
3231
+ const now = new Date().toISOString();
3232
+ const rawPayload = method === 'DELETE'
3233
+ ? { method: 'thread/goal/cleared', params: { threadId, source: 'supen', timestamp: now } }
3234
+ : { method: 'thread/goal/updated', params: { threadId, status, source: 'supen', timestamp: now } };
3235
+ const event = appendThreadEvent({
3236
+ threadId,
3237
+ runtimeThreadId: threadId,
3238
+ source: 'web',
3239
+ eventType: method === 'DELETE' ? 'thread_goal_cleared' : 'thread_goal_updated',
3240
+ rawPayload,
3241
+ receivedAt: now,
3242
+ });
3243
+ const chunk = threadStreamChunkForEvent(event);
3244
+ if (chunk) {
3245
+ broadcastToThread(threadId, attachThreadContextToChunk(chunk, readThreadContext({ threadId })), { id: event.sequence });
3246
+ }
3247
+ writeJson(res, 200, {
3248
+ ok: true,
3249
+ thread_id: threadId,
3250
+ goal: method === 'DELETE' ? null : { status },
3251
+ event_log_head: event.sequence,
3252
+ });
3253
+ return true;
3254
+ }
3326
3255
  if (codexProjectsOpenRoute(pathname) && method === 'POST') {
3327
3256
  try {
3328
3257
  const parsed = await readJsonBody(req);