@omercnet/paseo-omp 0.4.0-next.114.1 → 0.4.0-next.119.1

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.
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { constants, type Dir } from "node:fs";
3
3
  import { type FileHandle, lstat, open, opendir, realpath } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
5
+ import { basename, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { ompSessionDir } from "../paths";
7
7
  import { isValidImagePayload } from "./image";
8
8
 
@@ -510,16 +510,19 @@ interface PersistedTranscriptNode {
510
510
  message?: unknown;
511
511
  }
512
512
 
513
- function persistedTranscriptMessage(record: Record<string, unknown>, entryId: string): unknown {
513
+ function persistedTranscriptMessage(record: Record<string, unknown>, entryId?: string): unknown {
514
514
  if (record.type === "message") {
515
515
  if (!record.message || typeof record.message !== "object" || Array.isArray(record.message))
516
516
  return;
517
- return { ...(record.message as Record<string, unknown>), entryId };
517
+ return {
518
+ ...(record.message as Record<string, unknown>),
519
+ ...(entryId ? { entryId } : {}),
520
+ };
518
521
  }
519
522
  if (record.type !== "custom_message") return;
520
523
  return {
521
524
  role: "custom",
522
- entryId,
525
+ ...(entryId ? { entryId } : {}),
523
526
  customType: record.customType,
524
527
  content: record.content,
525
528
  display: record.display,
@@ -680,6 +683,7 @@ export async function readOmpPersistedSubagentTranscript(
680
683
  childTranscriptId: string,
681
684
  cwd: string,
682
685
  signal?: AbortSignal,
686
+ childSessionFile?: string,
683
687
  ): Promise<OmpPersistedSubagentTranscript> {
684
688
  signal?.throwIfAborted();
685
689
  if (
@@ -687,7 +691,12 @@ export async function readOmpPersistedSubagentTranscript(
687
691
  !parentSessionFile.endsWith(".jsonl") ||
688
692
  parentSessionFile.includes("\0") ||
689
693
  !CHILD_TRANSCRIPT_ID.test(childTranscriptId) ||
690
- basename(childTranscriptId) !== childTranscriptId
694
+ basename(childTranscriptId) !== childTranscriptId ||
695
+ (childSessionFile !== undefined &&
696
+ (!isAbsolute(childSessionFile) ||
697
+ !childSessionFile.endsWith(".jsonl") ||
698
+ childSessionFile.includes("\0") ||
699
+ basename(childSessionFile) !== `${childTranscriptId}.jsonl`))
691
700
  ) {
692
701
  throw new Error("Invalid OMP child transcript descriptor");
693
702
  }
@@ -707,34 +716,53 @@ export async function readOmpPersistedSubagentTranscript(
707
716
  await parentHandle.close().catch(() => undefined);
708
717
  }
709
718
  const canonicalParent = await realpath(parentSessionFile);
710
- const parentExtension = extname(canonicalParent);
711
- const expectedDirectory = canonicalParent.slice(0, -parentExtension.length);
719
+ const expectedDirectory = canonicalParent.slice(0, -".jsonl".length);
712
720
  const canonicalDirectory = await realpath(expectedDirectory).catch(() => undefined);
713
721
  if (!canonicalDirectory || canonicalDirectory !== expectedDirectory) {
714
722
  throw new Error("OMP child transcript directory is not canonically owned by its parent");
715
723
  }
716
- const sessionFile = join(canonicalDirectory, `${childTranscriptId}.jsonl`);
724
+ const requestedFile = resolve(
725
+ childSessionFile ?? join(canonicalDirectory, `${childTranscriptId}.jsonl`),
726
+ );
717
727
  let handle: FileHandle;
718
728
  try {
719
729
  handle = await open(
720
- sessionFile,
730
+ requestedFile,
721
731
  constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0),
722
732
  );
723
733
  } catch {
724
734
  throw new Error("OMP child transcript could not be opened");
725
735
  }
726
736
  try {
727
- const [stat, canonicalChild] = await Promise.all([handle.stat(), realpath(sessionFile)]);
737
+ const [stat, pathStat, canonicalChild] = await Promise.all([
738
+ handle.stat(),
739
+ lstat(requestedFile),
740
+ realpath(requestedFile),
741
+ ]);
728
742
  if (
729
743
  !stat.isFile() ||
730
- stat.size > MAX_CHILD_TRANSCRIPT_BYTES ||
731
- dirname(canonicalChild) !== canonicalDirectory ||
732
- canonicalChild !== sessionFile
744
+ !pathStat.isFile() ||
745
+ pathStat.isSymbolicLink() ||
746
+ stat.dev !== pathStat.dev ||
747
+ stat.ino !== pathStat.ino ||
748
+ stat.size > MAX_CHILD_TRANSCRIPT_BYTES
733
749
  ) {
734
750
  throw new Error("OMP child transcript failed ownership validation");
735
751
  }
736
- const bytes = await handle.readFile();
737
- signal?.throwIfAborted();
752
+ const canonicalRelativeChild = relative(canonicalDirectory, canonicalChild);
753
+ if (
754
+ !canonicalRelativeChild ||
755
+ canonicalRelativeChild.startsWith("..") ||
756
+ isAbsolute(canonicalRelativeChild)
757
+ ) {
758
+ throw new Error("OMP child transcript failed ownership validation");
759
+ }
760
+ const bytes = await readStableFile(
761
+ handle,
762
+ stat.size,
763
+ signal,
764
+ "OMP child transcript changed while reading",
765
+ );
738
766
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
739
767
  const messages: unknown[] = [];
740
768
  let nativeSessionId: string | undefined;
@@ -752,15 +780,21 @@ export async function readOmpPersistedSubagentTranscript(
752
780
  nativeSessionId ??= candidateId;
753
781
  if (nativeSessionId !== candidateId)
754
782
  throw new Error("OMP child transcript identity changed");
755
- } else if (record.type === "message" && record.message !== undefined) {
783
+ } else {
784
+ const entryId =
785
+ typeof record.id === "string" && CHILD_TRANSCRIPT_ID.test(record.id)
786
+ ? record.id
787
+ : undefined;
788
+ const message = persistedTranscriptMessage(record, entryId);
789
+ if (message === undefined) continue;
756
790
  if (messages.length >= MAX_CHILD_TRANSCRIPT_MESSAGES) {
757
791
  throw new Error("OMP child transcript exceeds message limits");
758
792
  }
759
- messages.push(record.message);
793
+ messages.push(message);
760
794
  }
761
795
  }
762
796
  if (!nativeSessionId) throw new Error("OMP child transcript is missing session identity");
763
- return { sessionFile, nativeSessionId, byteLength: bytes.byteLength, messages };
797
+ return { sessionFile: canonicalChild, nativeSessionId, byteLength: bytes.byteLength, messages };
764
798
  } catch (error) {
765
799
  if (error instanceof Error) throw error;
766
800
  throw new Error("OMP child transcript could not be decoded");
@@ -1685,7 +1685,7 @@ export class OmpProviderSession {
1685
1685
  });
1686
1686
  }
1687
1687
  }
1688
- messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
1688
+ messages ??= await waitForReplay(this.runtime.getMessages(replay.signal), replay.signal);
1689
1689
  this.quarantineBranchEntries();
1690
1690
  for (const message of messages) {
1691
1691
  replay.signal.throwIfAborted();
@@ -3234,6 +3234,17 @@ export class OmpProviderSession {
3234
3234
  this.handleRuntimeFailure();
3235
3235
  return;
3236
3236
  }
3237
+ if (
3238
+ event.type === "config_warnings_changed" ||
3239
+ event.type === "advisor_cost_changed" ||
3240
+ event.type === "ttsr_triggered"
3241
+ ) {
3242
+ return;
3243
+ }
3244
+ if (event.type === "irc_message") {
3245
+ this.projector.projectPassive(event);
3246
+ return;
3247
+ }
3237
3248
  if (
3238
3249
  event.type === "notice" ||
3239
3250
  event.type === "todo_reminder" ||
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { basename, extname } from "node:path";
2
+ import { basename, extname, isAbsolute } from "node:path";
3
3
  import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
4
4
  import { z } from "zod";
5
5
  import type {
@@ -882,10 +882,32 @@ export class OmpSubsessionProjector {
882
882
  signal.throwIfAborted();
883
883
  if (this.sessionIdByNativeId.has(snapshot.id)) continue;
884
884
  try {
885
- const history = await waitForReplay(
886
- runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
887
- signal,
888
- );
885
+ let history: ReplayHistory;
886
+ if (snapshot.sessionFile && isAbsolute(snapshot.sessionFile) && this.rootSessionFile) {
887
+ try {
888
+ history = await waitForReplay(
889
+ runtime.readPersistedSubagentTranscript({
890
+ parentSessionFile: this.rootSessionFile,
891
+ childTranscriptId: snapshot.id,
892
+ sessionFile: snapshot.sessionFile,
893
+ cwd: this.cwd,
894
+ signal,
895
+ }),
896
+ signal,
897
+ );
898
+ } catch (error) {
899
+ if (signal.aborted) throw error;
900
+ history = await waitForReplay(
901
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
902
+ signal,
903
+ );
904
+ }
905
+ } else {
906
+ history = await waitForReplay(
907
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
908
+ signal,
909
+ );
910
+ }
889
911
  try {
890
912
  this.accountReplay(history.messages, budget, signal);
891
913
  collected.push({
@@ -899,7 +921,7 @@ export class OmpSubsessionProjector {
899
921
  }
900
922
  } catch (error) {
901
923
  if (signal.aborted) throw error;
902
- collected.push({ snapshot });
924
+ collected.push({ snapshot, sessionFile: snapshot.sessionFile });
903
925
  }
904
926
  }
905
927
  collected.sort((left, right) => {
@@ -6,7 +6,12 @@ import type {
6
6
  } from "@getpaseo/plugin/server/provider";
7
7
  import { OMP_MCP_AUTH_TIMELINE_KIND } from "../../shared/mcp";
8
8
  import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
9
- import { OMP_MAX_CONTENT_PARTS, type OmpMessage, type OmpRpcEvent } from "./omp-rpc";
9
+ import {
10
+ OMP_MAX_CONTENT_PARTS,
11
+ OMP_MESSAGE_REPLAY_POLICIES,
12
+ type OmpMessage,
13
+ type OmpRpcEvent,
14
+ } from "./omp-rpc";
10
15
  import {
11
16
  boundedJsonBytes,
12
17
  type JsonValue,
@@ -134,6 +139,10 @@ function imageBlock(data: string, mimeType: string): StreamBlockSnapshot | undef
134
139
  }
135
140
 
136
141
  type OmpAssistantMessage = Extract<OmpMessage, { role: "assistant" }>;
142
+ type OmpCustomDisplayMessage = Extract<
143
+ OmpMessage,
144
+ { role: "bashExecution" | "pythonExecution" | "custom" | "hookMessage" }
145
+ >;
137
146
 
138
147
  function blockText(
139
148
  message: OmpAssistantMessage,
@@ -417,16 +426,35 @@ export class OmpTimelineProjector {
417
426
  return;
418
427
  }
419
428
  }
429
+ if (
430
+ (event.type === "message_start" ||
431
+ event.type === "message_update" ||
432
+ event.type === "message_end") &&
433
+ OMP_MESSAGE_REPLAY_POLICIES[event.message.role] === "ignore"
434
+ ) {
435
+ return;
436
+ }
420
437
  if (
421
438
  event.type === "todo_reminder" ||
422
439
  event.type === "todo_auto_clear" ||
440
+ event.type === "goal_updated" ||
441
+ event.type === "auto_retry_start" ||
442
+ event.type === "auto_retry_end" ||
443
+ event.type === "retry_fallback_applied" ||
444
+ event.type === "retry_fallback_succeeded" ||
423
445
  event.type === "notice" ||
424
446
  event.type === "extension_ui_request" ||
425
447
  event.type === "auto_compaction_start" ||
426
448
  event.type === "auto_compaction_end" ||
427
449
  event.type === "compaction_start" ||
428
450
  event.type === "compaction_end" ||
429
- event.type === "advisor_yielded"
451
+ event.type === "advisor_yielded" ||
452
+ event.type === "config_warnings_changed" ||
453
+ event.type === "advisor_cost_changed" ||
454
+ event.type === "ttsr_triggered" ||
455
+ event.type === "irc_message" ||
456
+ event.type === "model_changed" ||
457
+ event.type === "thinking_level_changed"
430
458
  ) {
431
459
  this.projectPassive(event);
432
460
  return;
@@ -449,7 +477,12 @@ export class OmpTimelineProjector {
449
477
  this.scheduleFlush();
450
478
  return;
451
479
  case "message_end":
452
- if (event.message.role === "custom" || event.message.role === "bashExecution") {
480
+ if (
481
+ event.message.role === "custom" ||
482
+ event.message.role === "hookMessage" ||
483
+ event.message.role === "bashExecution" ||
484
+ event.message.role === "pythonExecution"
485
+ ) {
453
486
  this.publishCustomMessage(event.message);
454
487
  return;
455
488
  }
@@ -499,12 +532,13 @@ export class OmpTimelineProjector {
499
532
  if (!snapshot.silent) this.publishTool(snapshot, "running");
500
533
  return;
501
534
  }
502
- case "tool_execution_update": {
535
+ case "tool_execution_update":
536
+ case "tool_stream_update": {
503
537
  const previous = this.tools.get(event.toolCallId);
504
538
  if (!previous) return;
505
539
  if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
506
540
  const output = this.dataFilter.json(
507
- event.partialResult,
541
+ event.type === "tool_execution_update" ? event.partialResult : event.update,
508
542
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
509
543
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
510
544
  );
@@ -600,6 +634,10 @@ export class OmpTimelineProjector {
600
634
  this.publishRetryFallback(event);
601
635
  return;
602
636
  }
637
+ if (event.type === "irc_message") {
638
+ this.publishCustomMessage(event.message as OmpCustomDisplayMessage);
639
+ return;
640
+ }
603
641
  if (event.type === "notice") {
604
642
  this.noticeSequence += 1;
605
643
  this.publish({
@@ -839,8 +877,9 @@ export class OmpTimelineProjector {
839
877
 
840
878
  projectReplayMessage(message: OmpMessage): void {
841
879
  if (this.closed) return;
842
- const nativeIdentity = assistantIdentity(message);
843
880
  this.replaySequence += 1;
881
+ if (OMP_MESSAGE_REPLAY_POLICIES[message.role] === "ignore") return;
882
+
844
883
  if (message.role === "user") {
845
884
  this.replayToolCalls.clear();
846
885
  if (this.replayTurnId) this.finishTurn(this.replayTurnId);
@@ -861,7 +900,9 @@ export class OmpTimelineProjector {
861
900
  }
862
901
  return;
863
902
  }
903
+
864
904
  if (message.role === "assistant") {
905
+ const nativeIdentity = assistantIdentity(message);
865
906
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
866
907
  this.projectingReplay = true;
867
908
  try {
@@ -880,6 +921,7 @@ export class OmpTimelineProjector {
880
921
  if (nativeIdentity) this.rememberReplayOccurrence(nativeIdentity, message);
881
922
  return;
882
923
  }
924
+
883
925
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
884
926
  if (message.role === "toolResult") {
885
927
  const replayCall = this.replayToolCalls.get(message.toolCallId);
@@ -910,10 +952,18 @@ export class OmpTimelineProjector {
910
952
  );
911
953
  return;
912
954
  }
913
- if (message.role === "bashExecution") {
955
+
956
+ if (
957
+ message.role === "bashExecution" ||
958
+ message.role === "pythonExecution" ||
959
+ message.role === "custom" ||
960
+ message.role === "hookMessage"
961
+ ) {
914
962
  this.publishCustomMessage(message);
915
- this.finishTurn(this.replayTurnId);
916
- this.replayTurnId = null;
963
+ if (message.role === "bashExecution" || message.role === "pythonExecution") {
964
+ this.finishTurn(this.replayTurnId);
965
+ this.replayTurnId = null;
966
+ }
917
967
  }
918
968
  }
919
969
 
@@ -1350,9 +1400,17 @@ export class OmpTimelineProjector {
1350
1400
  return undefined;
1351
1401
  }
1352
1402
 
1353
- private publishCustomMessage(message: OmpMessage): void {
1354
- if (message.display === false) return;
1355
- const rawType = message.customType ?? message.role;
1403
+ private publishCustomMessage(message: OmpCustomDisplayMessage): void {
1404
+ if (
1405
+ (message.role === "custom" || message.role === "hookMessage") &&
1406
+ message.display === false
1407
+ ) {
1408
+ return;
1409
+ }
1410
+ const rawType =
1411
+ message.role === "custom" || message.role === "hookMessage"
1412
+ ? (message.customType ?? message.role)
1413
+ : message.role;
1356
1414
  const publicType = this.dataFilter.text(rawType, 256);
1357
1415
  const lowerType = rawType.toLowerCase();
1358
1416
  const details = jsonRecord(this.dataFilter.json(message.details ?? null));
@@ -1361,7 +1419,9 @@ export class OmpTimelineProjector {
1361
1419
  const id = nativeIdentity
1362
1420
  ? `omp:custom:${createHash("sha256").update(nativeIdentity).digest("base64url").slice(0, 12)}`
1363
1421
  : `omp:custom:${this.customSequence}`;
1364
- const contentParts = Array.isArray(message.content) ? message.content : [];
1422
+ const messageContent =
1423
+ message.role === "custom" || message.role === "hookMessage" ? message.content : undefined;
1424
+ const contentParts = Array.isArray(messageContent) ? messageContent : [];
1365
1425
  const imageResult = nativeImageResult(
1366
1426
  {
1367
1427
  content:
@@ -1373,16 +1433,26 @@ export class OmpTimelineProjector {
1373
1433
  this.dataFilter,
1374
1434
  );
1375
1435
  const content =
1376
- typeof message.content === "string"
1377
- ? message.content
1436
+ typeof messageContent === "string"
1437
+ ? messageContent
1378
1438
  : contentParts
1379
1439
  .flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
1380
1440
  .join("\n\n");
1381
- if (message.role === "bashExecution" || /bash|shell|python/u.test(lowerType)) {
1441
+ const execution = message.role === "bashExecution" || message.role === "pythonExecution";
1442
+ if (execution || /bash|shell|python/u.test(lowerType)) {
1443
+ const nativeCommand =
1444
+ message.role === "bashExecution"
1445
+ ? message.command
1446
+ : message.role === "pythonExecution"
1447
+ ? message.code
1448
+ : undefined;
1382
1449
  const command = this.dataFilter.text(
1383
- message.command ?? firstString(details, "command", "input") ?? publicType,
1450
+ nativeCommand ?? firstString(details, "command", "input") ?? publicType,
1384
1451
  );
1385
- const output = message.output ?? content;
1452
+ const output = execution ? (message.output ?? content) : content;
1453
+ const exitCode = execution ? message.exitCode : undefined;
1454
+ const cancelled = execution ? message.cancelled : undefined;
1455
+ const truncated = execution ? message.truncated : undefined;
1386
1456
  this.publish({
1387
1457
  type: "tool_call",
1388
1458
  id,
@@ -1393,21 +1463,21 @@ export class OmpTimelineProjector {
1393
1463
  command,
1394
1464
  ...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
1395
1465
  ...(output ? { output: this.dataFilter.text(output, MAX_STREAM_TEXT_LENGTH) } : {}),
1396
- ...(typeof message.exitCode === "number" || message.exitCode === null
1397
- ? { exitCode: message.exitCode }
1466
+ ...(typeof exitCode === "number" || exitCode === null
1467
+ ? { exitCode }
1398
1468
  : typeof details?.exitCode === "number"
1399
1469
  ? { exitCode: details.exitCode }
1400
1470
  : {}),
1401
1471
  },
1402
- ...(message.cancelled !== undefined || message.truncated !== undefined
1472
+ ...(cancelled !== undefined || truncated !== undefined
1403
1473
  ? {
1404
1474
  metadata: {
1405
- ...(message.cancelled !== undefined ? { cancelled: message.cancelled } : {}),
1406
- ...(message.truncated !== undefined ? { truncated: message.truncated } : {}),
1475
+ ...(cancelled !== undefined ? { cancelled } : {}),
1476
+ ...(truncated !== undefined ? { truncated } : {}),
1407
1477
  },
1408
1478
  }
1409
1479
  : {}),
1410
- status: message.cancelled ? "canceled" : "completed",
1480
+ status: cancelled ? "canceled" : "completed",
1411
1481
  error: null,
1412
1482
  });
1413
1483
  if (imageResult) this.publishImages(id, publicType, imageResult.image);