@alook/daemon 0.1.19 → 0.1.20

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/dist/cli/index.js CHANGED
@@ -17679,7 +17679,7 @@ var AuditLogWakeTriggerPayloadSchema = exports_external.object({
17679
17679
  reason: exports_external.enum(["unread", "mention"])
17680
17680
  });
17681
17681
  var AuditLogSessionResetPayloadSchema = exports_external.object({
17682
- trigger: exports_external.enum(["single", "reset_all"])
17682
+ trigger: exports_external.enum(["single", "reset_all", "idle_timeout"])
17683
17683
  });
17684
17684
  var AuditLogNapPayloadSchema = exports_external.object({
17685
17685
  trigger: exports_external.literal("nap")
@@ -17722,10 +17722,33 @@ var BotAuditEventKindSchema = exports_external.enum([
17722
17722
  ]);
17723
17723
  var HostBotAuditEventFrameSchema = exports_external.object({
17724
17724
  type: exports_external.literal("bot_audit_event"),
17725
+ eventId: exports_external.string().min(1).max(128).optional(),
17726
+ occurredAt: exports_external.string().datetime({ offset: true }).optional(),
17725
17727
  agentId: exports_external.string().min(1),
17726
17728
  sessionId: exports_external.string().nullable().optional(),
17727
17729
  launchId: exports_external.string().nullable().optional(),
17728
17730
  event: BotAuditEventSchema
17731
+ }).superRefine((frame, ctx) => {
17732
+ if (frame.event.kind === "session_reset" && frame.event.payload.trigger === "idle_timeout" && (!frame.eventId || !frame.occurredAt)) {
17733
+ if (!frame.eventId) {
17734
+ ctx.addIssue({
17735
+ code: "custom",
17736
+ path: ["eventId"],
17737
+ message: "eventId is required for idle_timeout session_reset"
17738
+ });
17739
+ }
17740
+ if (!frame.occurredAt) {
17741
+ ctx.addIssue({
17742
+ code: "custom",
17743
+ path: ["occurredAt"],
17744
+ message: "occurredAt is required for idle_timeout session_reset"
17745
+ });
17746
+ }
17747
+ }
17748
+ });
17749
+ var BotAuditEventAckFrameSchema = exports_external.strictObject({
17750
+ type: exports_external.literal("bot_audit_event_ack"),
17751
+ eventId: exports_external.string().min(1).max(128)
17729
17752
  });
17730
17753
  // ../shared/src/db/community-schema.ts
17731
17754
  var exports_community_schema = {};
@@ -25708,6 +25731,7 @@ var DEFAULT_PING_INTERVAL_MS = 15000;
25708
25731
  var DEFAULT_PONG_TIMEOUT_MS = 30000;
25709
25732
  var DEFAULT_RECONNECT_BASE_MS = 500;
25710
25733
  var DEFAULT_RECONNECT_MAX_MS = 30000;
25734
+ var DEFAULT_AUDIT_ACK_RETRY_MS = 5000;
25711
25735
  function describeErr(err) {
25712
25736
  return err instanceof Error ? err.message : String(err);
25713
25737
  }
@@ -25723,8 +25747,10 @@ class WsControlChannel {
25723
25747
  closedByUser = false;
25724
25748
  authRejected = false;
25725
25749
  pingTimer = null;
25750
+ auditRetryTimer = null;
25726
25751
  pongDeadline = 0;
25727
25752
  resyncProvider = null;
25753
+ pendingBotAuditEvents = new Map;
25728
25754
  log;
25729
25755
  wakeCoordinator = new WakeCoordinator;
25730
25756
  constructor(opts) {
@@ -25742,6 +25768,7 @@ class WsControlChannel {
25742
25768
  close() {
25743
25769
  this.closedByUser = true;
25744
25770
  this.clearHeartbeat();
25771
+ this.clearAuditRetry();
25745
25772
  this.ws?.close();
25746
25773
  this.ws = null;
25747
25774
  this.statusValue = "closed";
@@ -25799,8 +25826,27 @@ class WsControlChannel {
25799
25826
  this.sendFrame({ type: "agent_typing_stop", ...info });
25800
25827
  }
25801
25828
  async reportBotAuditEvent(frame) {
25829
+ if (frame.event.kind === "session_reset" && frame.event.payload.trigger === "idle_timeout") {
25830
+ if (!frame.eventId || !frame.occurredAt) {
25831
+ this.log.error("durable idle-reset audit missing local receipt", {
25832
+ agentId: frame.agentId
25833
+ });
25834
+ return;
25835
+ }
25836
+ this.pendingBotAuditEvents.set(frame.eventId, frame);
25837
+ this.sendFrame(frame);
25838
+ this.scheduleAuditRetry();
25839
+ return;
25840
+ }
25802
25841
  this.sendFrame(frame);
25803
25842
  }
25843
+ restorePendingBotAuditEvent(frame) {
25844
+ if (!frame.eventId || !frame.occurredAt)
25845
+ return;
25846
+ this.pendingBotAuditEvents.set(frame.eventId, frame);
25847
+ this.sendFrame(frame);
25848
+ this.scheduleAuditRetry();
25849
+ }
25804
25850
  async reportWakeAck(info) {
25805
25851
  this.wakeCoordinator.recordDeliveryAck(info.agentId, info.launchId, info.status);
25806
25852
  this.sendFrame({ type: "agent_wake_ack", ...info });
@@ -25827,10 +25873,14 @@ class WsControlChannel {
25827
25873
  const liveActivities = activities ?? [];
25828
25874
  for (const a of liveActivities)
25829
25875
  this.sendFrame({ type: "agent_activity", ...a });
25876
+ for (const frame of this.pendingBotAuditEvents.values())
25877
+ this.sendFrame(frame);
25878
+ this.scheduleAuditRetry();
25830
25879
  this.log.info("resync sent", {
25831
25880
  ready: ready.runtimeReport.length,
25832
25881
  sessions: sessions.length,
25833
- activities: liveActivities.length
25882
+ activities: liveActivities.length,
25883
+ pendingAuditEvents: this.pendingBotAuditEvents.size
25834
25884
  });
25835
25885
  }
25836
25886
  for (const hook of this.resyncHooks) {
@@ -25873,6 +25923,34 @@ class WsControlChannel {
25873
25923
  this.opts.onAuthRejected?.();
25874
25924
  return;
25875
25925
  }
25926
+ const auditAck = BotAuditEventAckFrameSchema.safeParse(frame);
25927
+ if (auditAck.success) {
25928
+ this.attempt = 0;
25929
+ const pending = this.pendingBotAuditEvents.get(auditAck.data.eventId);
25930
+ if (!pending)
25931
+ return;
25932
+ let durableCleared = this.opts.onBotAuditEventAck === undefined;
25933
+ try {
25934
+ durableCleared = this.opts.onBotAuditEventAck?.({
25935
+ agentId: pending.agentId,
25936
+ eventId: auditAck.data.eventId
25937
+ }) ?? true;
25938
+ } catch (err) {
25939
+ this.log.warn("bot audit ack local clear threw", {
25940
+ eventId: auditAck.data.eventId,
25941
+ err: describeErr(err)
25942
+ });
25943
+ }
25944
+ if (durableCleared)
25945
+ this.pendingBotAuditEvents.delete(auditAck.data.eventId);
25946
+ else
25947
+ this.log.warn("bot audit ack local clear deferred", { eventId: auditAck.data.eventId });
25948
+ if (this.pendingBotAuditEvents.size === 0)
25949
+ this.clearAuditRetry();
25950
+ else
25951
+ this.scheduleAuditRetry();
25952
+ return;
25953
+ }
25876
25954
  this.attempt = 0;
25877
25955
  this.ingestCommand(frame).catch((err) => {
25878
25956
  this.log.warn("command ingress failed", { type: frame.type, err: describeErr(err) });
@@ -25947,6 +26025,7 @@ class WsControlChannel {
25947
26025
  onSocketClosed(code, reason) {
25948
26026
  this.log.warn("control channel closed", { code, reason: reason ? String(reason) : "" });
25949
26027
  this.clearHeartbeat();
26028
+ this.clearAuditRetry();
25950
26029
  this.ws = null;
25951
26030
  if (this.closedByUser)
25952
26031
  return;
@@ -25994,6 +26073,26 @@ class WsControlChannel {
25994
26073
  this.pingTimer = null;
25995
26074
  }
25996
26075
  }
26076
+ scheduleAuditRetry() {
26077
+ if (this.auditRetryTimer || this.pendingBotAuditEvents.size === 0)
26078
+ return;
26079
+ const delayMs = this.opts.auditAckRetryMs ?? DEFAULT_AUDIT_ACK_RETRY_MS;
26080
+ this.auditRetryTimer = setTimeout(() => {
26081
+ this.auditRetryTimer = null;
26082
+ if (this.statusValue === "open") {
26083
+ for (const frame of this.pendingBotAuditEvents.values())
26084
+ this.sendFrame(frame);
26085
+ }
26086
+ this.scheduleAuditRetry();
26087
+ }, delayMs);
26088
+ this.auditRetryTimer.unref?.();
26089
+ }
26090
+ clearAuditRetry() {
26091
+ if (!this.auditRetryTimer)
26092
+ return;
26093
+ clearTimeout(this.auditRetryTimer);
26094
+ this.auditRetryTimer = null;
26095
+ }
25997
26096
  now() {
25998
26097
  return this.opts.now ? this.opts.now() : Date.now();
25999
26098
  }
@@ -27697,14 +27796,14 @@ class AgentProcessManager {
27697
27796
  const effects = this.dispatch({ type: "wake", agentId, message: normalized, nowMs: this.now() });
27698
27797
  return effects.length > 0;
27699
27798
  }
27700
- forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
27701
- if (!this.forgetSessionSources(agentId, barrierType, forgottenSessionId))
27799
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId, pendingIdleResetEvent) {
27800
+ if (!this.forgetSessionSources(agentId, barrierType, forgottenSessionId, pendingIdleResetEvent))
27702
27801
  return false;
27703
27802
  this.dispatch({ type: "reset_session", agentId });
27704
27803
  return true;
27705
27804
  }
27706
- forgetSessionSources(agentId, barrierType, forgottenSessionId) {
27707
- const persisted = this.opts.timeline?.forgetSession(agentId, barrierType, forgottenSessionId);
27805
+ forgetSessionSources(agentId, barrierType, forgottenSessionId, pendingIdleResetEvent) {
27806
+ const persisted = pendingIdleResetEvent ? this.opts.timeline?.forgetSession(agentId, barrierType, forgottenSessionId, pendingIdleResetEvent) : this.opts.timeline?.forgetSession(agentId, barrierType, forgottenSessionId);
27708
27807
  if (persisted === false)
27709
27808
  return false;
27710
27809
  this.resumeSessions.delete(agentId);
@@ -28283,7 +28382,11 @@ ${this.opts.wakePromptFooter}` : text2;
28283
28382
  break;
28284
28383
  case "reset_idle_session": {
28285
28384
  const spawnState = this.activeSpawnState.get(effect.agentId);
28286
- const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId);
28385
+ const completion = {
28386
+ eventId: `bae_${randomUUID5()}`,
28387
+ occurredAt: new Date(this.now()).toISOString()
28388
+ };
28389
+ const persisted = this.forgetSession(effect.agentId, "reset_session", effect.sessionId, completion);
28287
28390
  if (!persisted) {
28288
28391
  this.log.error("idle session reset barrier was not persisted; reset deferred", {
28289
28392
  agentId: effect.agentId,
@@ -28295,6 +28398,23 @@ ${this.opts.wakePromptFooter}` : text2;
28295
28398
  if (spawnState)
28296
28399
  spawnState.discardEvents = true;
28297
28400
  this.dispatch({ type: "idle_reset_committed", agentId: effect.agentId, nowMs: this.now() });
28401
+ if (this.opts.onBotAuditEvent) {
28402
+ try {
28403
+ this.opts.onBotAuditEvent(effect.agentId, {
28404
+ kind: "session_reset",
28405
+ payload: { trigger: "idle_timeout" }
28406
+ }, {
28407
+ sessionId: null,
28408
+ launchId: null,
28409
+ ...completion
28410
+ });
28411
+ } catch (err) {
28412
+ this.log.debug("audit emit failed (idle session reset)", {
28413
+ agentId: effect.agentId,
28414
+ err: String(err)
28415
+ });
28416
+ }
28417
+ }
28298
28418
  this.log.info("idle agent session reset", {
28299
28419
  agentId: effect.agentId,
28300
28420
  sessionId: effect.sessionId
@@ -29317,11 +29437,13 @@ var TIMELINE_READ_CHUNK_BYTES = 65536;
29317
29437
  var DATE_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
29318
29438
  var RESUME_CONTROL_FILENAME = ".resume-control.json";
29319
29439
  var RESUME_CONTROL_MAX_BYTES = 4096;
29440
+ var MAX_PENDING_IDLE_RESET_EVENTS = 32;
29320
29441
  var EMPTY_RESUME_CONTROL = {
29321
29442
  version: 1,
29322
29443
  attemptedSessionId: null,
29323
29444
  fencedSessionId: null,
29324
- fullBarrier: null
29445
+ fullBarrier: null,
29446
+ pendingIdleResetEvents: []
29325
29447
  };
29326
29448
  function isBarrier(entry) {
29327
29449
  return entry.system !== undefined;
@@ -29711,7 +29833,8 @@ function readResumeControlState(timelineDir) {
29711
29833
  const raw = bounded.subarray(0, bytesRead).toString("utf8");
29712
29834
  const value = JSON.parse(raw);
29713
29835
  const validSessionId = (candidate) => candidate === null || typeof candidate === "string" && candidate.length > 0 && candidate.length <= 512;
29714
- if (value.version !== 1 || !validSessionId(value.attemptedSessionId) || !validSessionId(value.fencedSessionId) || value.fullBarrier !== null && value.fullBarrier !== "reset_session" && value.fullBarrier !== "nap")
29836
+ const pendingIdleResetEvents = value.pendingIdleResetEvents === undefined ? [] : value.pendingIdleResetEvents;
29837
+ if (value.version !== 1 || !validSessionId(value.attemptedSessionId) || !validSessionId(value.fencedSessionId) || !Array.isArray(pendingIdleResetEvents) || pendingIdleResetEvents.length > MAX_PENDING_IDLE_RESET_EVENTS || pendingIdleResetEvents.some((event) => !event || typeof event !== "object" || typeof event.eventId !== "string" || event.eventId.length < 1 || event.eventId.length > 128 || typeof event.occurredAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurredAt) || !Number.isFinite(Date.parse(event.occurredAt))) || new Set(pendingIdleResetEvents.map((event) => event.eventId)).size !== pendingIdleResetEvents.length || value.fullBarrier !== null && value.fullBarrier !== "reset_session" && value.fullBarrier !== "nap")
29715
29838
  return { kind: "invalid" };
29716
29839
  return {
29717
29840
  kind: "state",
@@ -29719,7 +29842,8 @@ function readResumeControlState(timelineDir) {
29719
29842
  version: 1,
29720
29843
  attemptedSessionId: value.attemptedSessionId,
29721
29844
  fencedSessionId: value.fencedSessionId,
29722
- fullBarrier: value.fullBarrier
29845
+ fullBarrier: value.fullBarrier,
29846
+ pendingIdleResetEvents
29723
29847
  }
29724
29848
  };
29725
29849
  } catch {
@@ -29742,11 +29866,14 @@ function updateResumeControlState(timelineDir, update) {
29742
29866
  const current = readResumeControlState(timelineDir);
29743
29867
  const base = current.kind === "state" ? current.state : EMPTY_RESUME_CONTROL;
29744
29868
  const next = update({ ...base });
29869
+ if (!Array.isArray(next.pendingIdleResetEvents) || next.pendingIdleResetEvents.length > MAX_PENDING_IDLE_RESET_EVENTS || next.pendingIdleResetEvents.some((event) => !event || typeof event !== "object" || typeof event.eventId !== "string" || event.eventId.length < 1 || event.eventId.length > 128 || typeof event.occurredAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.occurredAt) || !Number.isFinite(Date.parse(event.occurredAt))) || new Set(next.pendingIdleResetEvents.map((event) => event.eventId)).size !== next.pendingIdleResetEvents.length)
29870
+ return false;
29745
29871
  const canonical = {
29746
29872
  version: 1,
29747
29873
  attemptedSessionId: next.attemptedSessionId,
29748
29874
  fencedSessionId: next.fencedSessionId,
29749
- fullBarrier: next.fullBarrier
29875
+ fullBarrier: next.fullBarrier,
29876
+ pendingIdleResetEvents: next.pendingIdleResetEvents
29750
29877
  };
29751
29878
  const body = JSON.stringify(canonical) + `
29752
29879
  `;
@@ -30410,7 +30537,7 @@ function createTimelineRecorder(opts) {
30410
30537
  clearSessionStall(agentId, sessionId) {
30411
30538
  return appendStallMarker(agentId, "stall_recovery_clear", sessionId);
30412
30539
  },
30413
- forgetSession(agentId, barrierType = "reset_session", forgottenSessionId) {
30540
+ forgetSession(agentId, barrierType = "reset_session", forgottenSessionId, pendingIdleResetEvent) {
30414
30541
  retryPending(agentId);
30415
30542
  const dir = dirFor(agentId);
30416
30543
  if (!prepareTimelineDirectory(dir))
@@ -30422,7 +30549,8 @@ function createTimelineRecorder(opts) {
30422
30549
  ...state,
30423
30550
  attemptedSessionId: null,
30424
30551
  fencedSessionId: null,
30425
- fullBarrier: barrierType
30552
+ fullBarrier: barrierType,
30553
+ pendingIdleResetEvents: pendingIdleResetEvent && !state.pendingIdleResetEvents.some(({ eventId }) => eventId === pendingIdleResetEvent.eventId) ? [...state.pendingIdleResetEvents, pendingIdleResetEvent] : state.pendingIdleResetEvents
30426
30554
  }));
30427
30555
  } else if (barrierType === "stall_recovery") {
30428
30556
  persisted = updateResumeControlState(dir, (state) => ({
@@ -30453,6 +30581,19 @@ function createTimelineRecorder(opts) {
30453
30581
  const result = appendTrackedEntry(dir, createSystemEntry(barrierType, stamp.toISOString(), forgottenSessionId), stamp);
30454
30582
  handleTrackedResult(agentId, null, result);
30455
30583
  return true;
30584
+ },
30585
+ pendingIdleResetEvents(agentId) {
30586
+ const state = readResumeControlState(dirFor(agentId));
30587
+ return state.kind === "state" ? state.state.pendingIdleResetEvents.map((event) => ({ ...event })) : [];
30588
+ },
30589
+ acknowledgeIdleResetEvent(agentId, eventId) {
30590
+ const dir = dirFor(agentId);
30591
+ if (!prepareTimelineDirectory(dir))
30592
+ return false;
30593
+ return updateResumeControlState(dir, (state) => ({
30594
+ ...state,
30595
+ pendingIdleResetEvents: state.pendingIdleResetEvents.filter((event) => event.eventId !== eventId)
30596
+ }));
30456
30597
  }
30457
30598
  };
30458
30599
  function appendStallMarker(agentId, type, sessionId) {
@@ -31007,6 +31148,8 @@ async function createDaemon(opts) {
31007
31148
  const emitBotAuditEvent = (agentId, event, context) => {
31008
31149
  channelRef?.reportBotAuditEvent?.({
31009
31150
  type: "bot_audit_event",
31151
+ ...context?.eventId ? { eventId: context.eventId } : {},
31152
+ ...context?.occurredAt ? { occurredAt: context.occurredAt } : {},
31010
31153
  agentId,
31011
31154
  sessionId: context?.sessionId ?? null,
31012
31155
  launchId: context?.launchId ?? null,
@@ -31106,6 +31249,7 @@ async function createDaemon(opts) {
31106
31249
  ownerName: b.ownerName,
31107
31250
  ownerDiscriminator: b.ownerDiscriminator
31108
31251
  });
31252
+ restorePendingIdleResetEvents(b.id);
31109
31253
  }
31110
31254
  botCacheReady = true;
31111
31255
  }
@@ -31200,9 +31344,23 @@ async function createDaemon(opts) {
31200
31344
  headers: { Authorization: `Bearer ${opts.machineKey}` },
31201
31345
  webSocketFactory: opts.webSocketFactory,
31202
31346
  onAuthRejected: opts.onAuthRejected,
31347
+ onBotAuditEventAck: ({ agentId, eventId }) => timeline2.acknowledgeIdleResetEvent(agentId, eventId),
31203
31348
  logger: log2.child("ws")
31204
31349
  });
31205
31350
  channelRef = channel2;
31351
+ function restorePendingIdleResetEvents(agentId) {
31352
+ for (const pending of timeline2.pendingIdleResetEvents(agentId)) {
31353
+ channel2.restorePendingBotAuditEvent({
31354
+ type: "bot_audit_event",
31355
+ eventId: pending.eventId,
31356
+ occurredAt: pending.occurredAt,
31357
+ agentId,
31358
+ sessionId: null,
31359
+ launchId: null,
31360
+ event: { kind: "session_reset", payload: { trigger: "idle_timeout" } }
31361
+ });
31362
+ }
31363
+ }
31206
31364
  function handleBotFrame(cmd) {
31207
31365
  switch (cmd.type) {
31208
31366
  case "bot:added":
@@ -31213,6 +31371,7 @@ async function createDaemon(opts) {
31213
31371
  ownerName: cmd.ownerName,
31214
31372
  ownerDiscriminator: cmd.ownerDiscriminator
31215
31373
  });
31374
+ restorePendingIdleResetEvents(cmd.botId);
31216
31375
  log2.debug("bot:added", { botId: cmd.botId, name: cmd.name });
31217
31376
  break;
31218
31377
  case "bot:updated": {