pi-agent-browser-native 0.3.0 → 0.5.0

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 (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. package/scripts/platform-smoke/targets.mjs +12 -6
@@ -1,3 +1,4 @@
1
+ import { getAgentBrowserSessionIdentityKey } from "../argv-grammar.js";
1
2
  import { isRecord } from "../parsing.js";
2
3
  export function isPendingRecordingCommand(command, subcommand, kind) {
3
4
  return command === "record" && (subcommand === "start" || subcommand === "restart") && kind === "video";
@@ -32,6 +33,16 @@ function isManifestEntry(value) {
32
33
  return false;
33
34
  if (typeof value.kind !== "string" || value.kind.trim().length === 0)
34
35
  return false;
36
+ for (const key of ["absolutePath", "command", "cwd", "extension", "mediaType", "namespace", "requestedPath", "session", "subcommand"]) {
37
+ if (value[key] !== undefined && typeof value[key] !== "string")
38
+ return false;
39
+ }
40
+ if (value.evictedAtMs !== undefined && (typeof value.evictedAtMs !== "number" || !Number.isFinite(value.evictedAtMs)))
41
+ return false;
42
+ if (value.exists !== undefined && typeof value.exists !== "boolean")
43
+ return false;
44
+ if (value.sizeBytes !== undefined && (typeof value.sizeBytes !== "number" || !Number.isFinite(value.sizeBytes) || value.sizeBytes < 0))
45
+ return false;
35
46
  return true;
36
47
  }
37
48
  export function isSessionArtifactManifest(value) {
@@ -73,7 +84,34 @@ export function formatSessionArtifactRetentionSummary(manifest) {
73
84
  return `Session artifacts: ${parts.join(", ")} (${manifest.entries.length}/${manifest.maxEntries} recent).`;
74
85
  }
75
86
  export function getSessionArtifactManifestEntryKey(entry) {
76
- return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
87
+ const pathKey = entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
88
+ const recordingSessionKey = entry.command === "record" && entry.kind === "video" && entry.session
89
+ ? getAgentBrowserSessionIdentityKey(entry.session, entry.namespace)
90
+ : undefined;
91
+ return recordingSessionKey ? `${pathKey}\0${recordingSessionKey}` : pathKey;
92
+ }
93
+ export function retirePendingRecordingManifestEntries(manifest, sessionName, namespace, nowMs = Date.now()) {
94
+ let changed = false;
95
+ const sessionKey = sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : undefined;
96
+ const entries = manifest.entries.map((entry) => {
97
+ if (!sessionKey
98
+ || !entry.session
99
+ || getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) !== sessionKey
100
+ || entry.kind !== "video"
101
+ || !isPendingRecordingCommand(entry.command, entry.subcommand, entry.kind))
102
+ return entry;
103
+ changed = true;
104
+ return { ...entry, retentionState: "missing", subcommand: "close-abandoned" };
105
+ });
106
+ if (!changed)
107
+ return manifest;
108
+ return {
109
+ ...manifest,
110
+ entries,
111
+ evictedCount: entries.filter((entry) => entry.retentionState === "evicted").length,
112
+ liveCount: entries.filter((entry) => entry.retentionState === "live").length,
113
+ updatedAtMs: nowMs,
114
+ };
77
115
  }
78
116
  export function mergeSessionArtifactManifest(options) {
79
117
  const nowMs = options.nowMs ?? Date.now();
@@ -82,13 +120,31 @@ export function mergeSessionArtifactManifest(options) {
82
120
  for (const entry of options.base?.entries ?? []) {
83
121
  byPath.set(getSessionArtifactManifestEntryKey(entry), entry);
84
122
  }
85
- for (const entry of options.entries ?? []) {
123
+ const orderedEntries = (options.entries ?? [])
124
+ .map((entry, index) => ({ entry, index }))
125
+ .sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs || left.index - right.index)
126
+ .map(({ entry }) => entry);
127
+ for (const entry of orderedEntries) {
86
128
  const key = getSessionArtifactManifestEntryKey(entry);
129
+ if (entry.command === "record" && entry.kind === "video") {
130
+ const entrySessionKey = entry.session ? getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) : undefined;
131
+ for (const [candidateKey, candidate] of byPath) {
132
+ const sameRecordingSession = entrySessionKey === undefined
133
+ ? candidate.session === undefined
134
+ : candidate.session !== undefined && getAgentBrowserSessionIdentityKey(candidate.session, candidate.namespace) === entrySessionKey;
135
+ if (candidateKey !== key
136
+ && sameRecordingSession
137
+ && candidate.kind === "video"
138
+ && isPendingRecordingCommand(candidate.command, candidate.subcommand, candidate.kind)) {
139
+ byPath.delete(candidateKey);
140
+ }
141
+ }
142
+ }
87
143
  const existing = byPath.get(key);
88
144
  byPath.set(key, {
89
145
  ...existing,
90
146
  ...entry,
91
- createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
147
+ createdAtMs: entry.command === "record" && entry.kind === "video" ? entry.createdAtMs : existing?.createdAtMs ?? entry.createdAtMs,
92
148
  evictedAtMs: entry.retentionState === "evicted" ? (entry.evictedAtMs ?? nowMs) : entry.evictedAtMs,
93
149
  });
94
150
  }
@@ -98,7 +154,9 @@ export function mergeSessionArtifactManifest(options) {
98
154
  .sort((left, right) => {
99
155
  const leftTime = left.evictedAtMs ?? left.createdAtMs;
100
156
  const rightTime = right.evictedAtMs ?? right.createdAtMs;
101
- return rightTime - leftTime || left.path.localeCompare(right.path);
157
+ return rightTime - leftTime
158
+ || Number(isPendingRecordingCommand(right.command, right.subcommand, right.kind)) - Number(isPendingRecordingCommand(left.command, left.subcommand, left.kind))
159
+ || left.path.localeCompare(right.path);
102
160
  })
103
161
  .slice(0, maxEntries);
104
162
  return {
@@ -5,6 +5,8 @@ function hasUnverifiedFileArtifact(artifacts) {
5
5
  export function classifyAgentBrowserSuccessCategory(options) {
6
6
  if (options.inspection)
7
7
  return "inspection";
8
+ if ((options.artifacts ?? []).some(isPendingRecordingArtifact))
9
+ return "artifact-pending";
8
10
  if ((options.artifacts ?? []).length > 0)
9
11
  return hasUnverifiedFileArtifact(options.artifacts) ? "artifact-unverified" : "artifact-saved";
10
12
  if (options.savedFile)
@@ -19,6 +21,9 @@ export function classifyAgentBrowserFailureCategory(options) {
19
21
  // missed control named "Confirmation required" still gets selector recovery.
20
22
  if (options.confirmationRequired)
21
23
  return "confirmation-required";
24
+ // Canonical upstream prefix first: lastUrl can contain aborted / policy-blocked / about:blank.
25
+ if (/\btab_gone:/i.test(text))
26
+ return "tab-gone";
22
27
  // Upstream 0.32.4+ locator misses keep detail and may echo getByRole/getByText or Names seen lists.
23
28
  // Evaluate before text-derived timeout/confirmation so accessible-name substrings cannot suppress recovery.
24
29
  const isUpstreamLocatorMiss = /\bNo element found:\s*(?:getBy[A-Za-z]+|role=|text=|label=|placeholder=|alt=|title=|testid=)/i.test(text) ||
@@ -48,7 +53,7 @@ export function classifyAgentBrowserFailureCategory(options) {
48
53
  return "policy-blocked";
49
54
  if (/cleanup failed|cleanup.*partial|partial cleanup|remaining resources/i.test(text))
50
55
  return "cleanup-failed";
51
- if (options.validationError)
56
+ if (options.validationError || /Agent-browser Unix socket path would be|Agent-browser socket storage .* is unusable/i.test(text))
52
57
  return "validation-error";
53
58
  if (options.tabDrift || /could not re-select the intended tab|about:blank|selected tab looks wrong|tab drift|tab.*wrong/i.test(text))
54
59
  return "tab-drift";
@@ -1,15 +1,15 @@
1
1
  export function withOptionalNamespaceArgs(namespace, args) {
2
- return namespace && args[0] !== "--namespace" ? ["--namespace", namespace, ...args] : args;
2
+ return namespace !== undefined && args[0] !== "--namespace" ? ["--namespace", namespace, ...args] : args;
3
3
  }
4
4
  export function withOptionalSessionArgs(sessionName, args) {
5
- if (!sessionName || args[0] === "--session")
5
+ if (!sessionName || args[0] === "--session" || (args[0] === "--namespace" && args[2] === "--session"))
6
6
  return args;
7
- if (args[0] === "--namespace" && args[1] && args[2] !== "--session")
7
+ if (args[0] === "--namespace" && args.length >= 2)
8
8
  return [args[0], args[1], "--session", sessionName, ...args.slice(2)];
9
9
  return ["--session", sessionName, ...args];
10
10
  }
11
11
  export function applyNamespaceToNextActions(actions, namespace) {
12
- if (!namespace || !actions)
12
+ if (namespace === undefined || !actions)
13
13
  return actions;
14
14
  return actions.map((action) => {
15
15
  const args = action.params?.args;
@@ -19,6 +19,18 @@ export function applyNamespaceToNextActions(actions, namespace) {
19
19
  return networkSourceLookup ? { ...action, params: { ...action.params, networkSourceLookup: { ...networkSourceLookup, namespace } } } : action;
20
20
  });
21
21
  }
22
+ export function applySessionToNextActions(actions, sessionName) {
23
+ if (!sessionName || !actions)
24
+ return actions;
25
+ return actions.map((action) => {
26
+ // Fresh-session actions deliberately target a new session; the planner ignores sessionMode when an
27
+ // explicit --session is present, so prefixing one here would silently downgrade them to reuse.
28
+ if (action.params?.sessionMode === "fresh")
29
+ return action;
30
+ const args = action.params?.args;
31
+ return args ? { ...action, params: { ...action.params, args: withOptionalSessionArgs(sessionName, args) } } : action;
32
+ });
33
+ }
22
34
  export function buildNextToolAction(options) {
23
35
  return {
24
36
  id: options.id,
@@ -48,7 +60,9 @@ export function isStandaloneSnapshotNextAction(action) {
48
60
  const args = action.params?.args;
49
61
  if (!args || action.params?.stdin)
50
62
  return false;
51
- const commandIndex = args[0] === "--session" ? 2 : 0;
63
+ let commandIndex = args[0] === "--namespace" ? 2 : 0;
64
+ if (args[commandIndex] === "--session")
65
+ commandIndex += 2;
52
66
  return args[commandIndex] === "snapshot";
53
67
  }
54
68
  export function alignPageChangeSummaryNextActionIds(summary, nextActions) {
@@ -1,7 +1,8 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
2
  import { extname, resolve } from "node:path";
3
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
3
4
  import { isRecord, parsePositiveInteger } from "../../parsing.js";
4
- import { formatSessionArtifactRetentionSummary, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
5
+ import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
5
6
  import { classifyAgentBrowserSuccessCategory } from "../categories.js";
6
7
  const IMAGE_EXTENSION_TO_MIME_TYPE = {
7
8
  ".gif": "image/gif",
@@ -12,6 +13,7 @@ const IMAGE_EXTENSION_TO_MIME_TYPE = {
12
13
  };
13
14
  const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
14
15
  const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
16
+ const ARTIFACT_MTIME_TOLERANCE_MS = 2_000;
15
17
  function getImageMimeType(filePath) {
16
18
  const extension = extname(filePath).toLowerCase();
17
19
  return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
@@ -36,14 +38,11 @@ function appendPresentationNotice(presentation, message) {
36
38
  function shouldAppendArtifactRetentionNotice(entries) {
37
39
  return entries.some((entry) => entry.retentionState === "evicted" || entry.storageScope !== "explicit-path");
38
40
  }
39
- function getManifestEntryKey(entry) {
40
- return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
41
- }
42
41
  export function manifestHasNewNoticeWorthyEntries(base, current) {
43
42
  if (!current)
44
43
  return false;
45
- const baseKeys = new Set((base?.entries ?? []).map(getManifestEntryKey));
46
- return current.entries.some((entry) => !baseKeys.has(getManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
44
+ const baseKeys = new Set((base?.entries ?? []).map(getSessionArtifactManifestEntryKey));
45
+ return current.entries.some((entry) => !baseKeys.has(getSessionArtifactManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
47
46
  }
48
47
  export function applyArtifactManifest(presentation, baseManifest, entries) {
49
48
  if (entries.length === 0)
@@ -86,6 +85,9 @@ const ARTIFACT_EXTENSION_TO_MEDIA_TYPE = {
86
85
  ".zip": "application/zip",
87
86
  ...IMAGE_EXTENSION_TO_MIME_TYPE,
88
87
  };
88
+ function isDownloadWaitSubcommand(subcommand) {
89
+ return subcommand === "--download" || subcommand === "-d";
90
+ }
89
91
  function getArtifactKind(commandInfo) {
90
92
  if (commandInfo.command === "screenshot")
91
93
  return "image";
@@ -95,7 +97,7 @@ function getArtifactKind(commandInfo) {
95
97
  return "pdf";
96
98
  if (commandInfo.command === "download")
97
99
  return "download";
98
- if (commandInfo.command === "wait" && commandInfo.subcommand === "--download")
100
+ if (commandInfo.command === "wait" && isDownloadWaitSubcommand(commandInfo.subcommand))
99
101
  return "download";
100
102
  if (commandInfo.command === "state" && commandInfo.subcommand === "save")
101
103
  return "file";
@@ -135,6 +137,11 @@ function extractPathStrings(data) {
135
137
  }
136
138
  return [...new Set(paths)];
137
139
  }
140
+ function artifactMtimeIsOutsideCommandWindow(updatedAtMs, minUpdatedAtMs, maxUpdatedAtMs) {
141
+ return minUpdatedAtMs !== undefined
142
+ && (updatedAtMs < minUpdatedAtMs - ARTIFACT_MTIME_TOLERANCE_MS
143
+ || (maxUpdatedAtMs !== undefined && updatedAtMs > maxUpdatedAtMs + ARTIFACT_MTIME_TOLERANCE_MS));
144
+ }
138
145
  async function buildFileArtifactMetadata(options) {
139
146
  const kind = getArtifactKind(options.commandInfo);
140
147
  if (!kind) {
@@ -146,11 +153,16 @@ async function buildFileArtifactMetadata(options) {
146
153
  const pendingRecording = isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind);
147
154
  let exists;
148
155
  let sizeBytes;
156
+ let stale = false;
157
+ let updatedAtMs;
149
158
  if (!pendingRecording) {
150
159
  try {
151
160
  const fileStats = await stat(absolutePath);
152
161
  exists = true;
153
162
  sizeBytes = fileStats.size;
163
+ updatedAtMs = fileStats.mtimeMs;
164
+ const commandCreatesArtifact = !(options.commandInfo.command === "wait" && isDownloadWaitSubcommand(options.commandInfo.subcommand));
165
+ stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
154
166
  }
155
167
  catch {
156
168
  exists = false;
@@ -165,31 +177,33 @@ async function buildFileArtifactMetadata(options) {
165
177
  extension,
166
178
  kind,
167
179
  mediaType: extension ? ARTIFACT_EXTENSION_TO_MEDIA_TYPE[extension] : undefined,
180
+ namespace: options.namespace,
168
181
  path: displayPath,
169
182
  recordingState: pendingRecording ? "openRecording" : undefined,
170
183
  requestedPath: options.artifactRequest?.path,
171
184
  session: options.sessionName,
172
185
  sizeBytes,
173
- status: options.artifactRequest?.status ?? (pendingRecording ? "pending" : exists === false ? "missing" : "saved"),
186
+ status: pendingRecording ? "pending" : exists === false ? "missing" : stale ? "stale" : options.artifactRequest?.status ?? "saved",
174
187
  subcommand: options.commandInfo.subcommand,
175
188
  tempPath: options.artifactRequest?.tempPath,
189
+ updatedAtMs,
176
190
  willExistOnStop: pendingRecording ? true : undefined,
177
191
  };
178
192
  }
179
193
  async function buildPreviousRestartRecordingArtifact(options) {
180
194
  if (options.commandInfo.command !== "record" || options.commandInfo.subcommand !== "restart")
181
195
  return undefined;
196
+ const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
182
197
  const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
183
198
  (entry.subcommand === "start" || entry.subcommand === "restart") &&
184
199
  entry.kind === "video" &&
185
- (!options.sessionName || !entry.session || entry.session === options.sessionName) &&
186
- !options.currentPaths.has(entry.path) &&
187
- (!entry.absolutePath || !options.currentPaths.has(entry.absolutePath))));
200
+ (!sessionKey || (entry.session && getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) === sessionKey))));
188
201
  if (!previousRecording)
189
202
  return undefined;
190
203
  const absolutePath = previousRecording.absolutePath ?? resolve(options.cwd, previousRecording.path);
191
204
  try {
192
205
  const fileStats = await stat(absolutePath);
206
+ const stale = artifactMtimeIsOutsideCommandWindow(fileStats.mtimeMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
193
207
  return {
194
208
  absolutePath,
195
209
  artifactType: "video",
@@ -199,23 +213,39 @@ async function buildPreviousRestartRecordingArtifact(options) {
199
213
  extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
200
214
  kind: "video",
201
215
  mediaType: previousRecording.mediaType,
216
+ namespace: previousRecording.namespace ?? options.namespace,
202
217
  path: previousRecording.path,
203
218
  requestedPath: previousRecording.requestedPath,
204
219
  session: previousRecording.session ?? options.sessionName,
205
220
  sizeBytes: fileStats.size,
206
- status: "saved",
221
+ status: stale ? "stale" : "saved",
207
222
  subcommand: "restart-previous",
223
+ updatedAtMs: fileStats.mtimeMs,
208
224
  };
209
225
  }
210
226
  catch {
211
- return undefined;
227
+ return {
228
+ absolutePath,
229
+ artifactType: "video",
230
+ command: "record",
231
+ cwd: previousRecording.cwd ?? options.cwd,
232
+ exists: false,
233
+ extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
234
+ kind: "video",
235
+ mediaType: previousRecording.mediaType,
236
+ namespace: previousRecording.namespace ?? options.namespace,
237
+ path: previousRecording.path,
238
+ requestedPath: previousRecording.requestedPath,
239
+ session: previousRecording.session ?? options.sessionName,
240
+ status: "missing",
241
+ subcommand: "restart-previous",
242
+ };
212
243
  }
213
244
  }
214
245
  export async function extractFileArtifacts(options) {
215
246
  const candidates = extractPathStrings(options.data);
216
247
  const currentArtifacts = (await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path })))).filter((artifact) => artifact !== undefined);
217
- const currentPaths = new Set(currentArtifacts.flatMap((artifact) => [artifact.path, artifact.absolutePath]));
218
- const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact({ artifactManifest: options.artifactManifest, commandInfo: options.commandInfo, currentPaths, cwd: options.cwd, sessionName: options.sessionName });
248
+ const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact({ artifactManifest: options.artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, commandInfo: options.commandInfo, cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
219
249
  return previousRestartRecordingArtifact ? [previousRestartRecordingArtifact, ...currentArtifacts] : currentArtifacts;
220
250
  }
221
251
  export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now()) {
@@ -228,9 +258,10 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
228
258
  extension: artifact.extension,
229
259
  kind: artifact.kind,
230
260
  mediaType: artifact.mediaType,
261
+ namespace: artifact.namespace,
231
262
  path: artifact.path,
232
263
  requestedPath: artifact.requestedPath,
233
- retentionState: artifact.exists === false ? "missing" : "live",
264
+ retentionState: artifact.exists === false || artifact.status === "stale" ? "missing" : "live",
234
265
  session: artifact.session,
235
266
  sizeBytes: artifact.sizeBytes,
236
267
  storageScope: "explicit-path",
@@ -238,6 +269,9 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
238
269
  }));
239
270
  }
240
271
  export function isManifestFileArtifact(artifact) {
272
+ if (artifact.status === "stale") {
273
+ return artifact.kind === "video" && artifact.command === "record" && artifact.subcommand === "restart-previous";
274
+ }
241
275
  return artifact.kind === "video" && artifact.command === "record" ? true : !isPendingRecordingArtifact(artifact);
242
276
  }
243
277
  function getArtifactVerificationEntry(artifact) {
@@ -259,20 +293,24 @@ function getArtifactVerificationEntry(artifact) {
259
293
  willExistOnStop: artifact.willExistOnStop ?? true,
260
294
  };
261
295
  }
262
- const state = artifact.exists === true
263
- ? "verified"
264
- : artifact.exists === false
265
- ? "missing"
266
- : "unverified";
296
+ const state = artifact.status === "stale"
297
+ ? "unverified"
298
+ : artifact.exists === true
299
+ ? "verified"
300
+ : artifact.exists === false
301
+ ? "missing"
302
+ : "unverified";
267
303
  return {
268
304
  absolutePath: artifact.absolutePath,
269
305
  exists: artifact.exists,
270
306
  kind: artifact.kind,
271
- limitation: state === "missing"
272
- ? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
273
- : state === "unverified"
274
- ? "The wrapper could not prove local filesystem existence for this artifact."
275
- : undefined,
307
+ limitation: artifact.status === "stale"
308
+ ? "The reported path's modification time fell outside this command's bounded artifact window. Treat the artifact as stale until regenerated."
309
+ : state === "missing"
310
+ ? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
311
+ : state === "unverified"
312
+ ? "The wrapper could not prove local filesystem existence for this artifact."
313
+ : undefined,
276
314
  mediaType: artifact.mediaType,
277
315
  path: artifact.path,
278
316
  requestedPath: artifact.requestedPath,
@@ -281,6 +319,7 @@ function getArtifactVerificationEntry(artifact) {
281
319
  state,
282
320
  status: artifact.status,
283
321
  storageScope: "explicit-path",
322
+ updatedAtMs: artifact.updatedAtMs,
284
323
  };
285
324
  }
286
325
  function getManifestVerificationEntry(entry) {
@@ -335,17 +374,19 @@ export function buildArtifactVerificationSummary(artifacts, manifest, manifestPa
335
374
  };
336
375
  }
337
376
  export function hasMissingFileArtifact(artifacts) {
338
- return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists === false);
377
+ return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists === false || artifact.status === "stale"));
339
378
  }
340
379
  export function formatMissingArtifactFailureText(artifacts) {
341
- const missingArtifacts = (artifacts ?? []).filter((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists === false);
342
- if (missingArtifacts.length === 0)
380
+ const failedArtifacts = (artifacts ?? []).filter((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists === false || artifact.status === "stale"));
381
+ if (failedArtifacts.length === 0)
343
382
  return undefined;
344
- if (missingArtifacts.length === 1) {
345
- const artifact = missingArtifacts[0];
346
- return `Artifact verification failed: requested ${artifact.kind} was not found at ${artifact.absolutePath}.`;
383
+ if (failedArtifacts.length === 1) {
384
+ const artifact = failedArtifacts[0];
385
+ return artifact.status === "stale"
386
+ ? `Artifact verification failed: requested ${artifact.kind} path ${artifact.absolutePath} has a modification time outside the current command window.`
387
+ : `Artifact verification failed: requested ${artifact.kind} was not found at ${artifact.absolutePath}.`;
347
388
  }
348
- return `Artifact verification failed: ${missingArtifacts.length} requested artifacts were not found on disk.`;
389
+ return `Artifact verification failed: ${failedArtifacts.length} requested artifacts were missing or stale.`;
349
390
  }
350
391
  export function classifyPresentationSuccessCategory(options) {
351
392
  if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
@@ -357,9 +398,9 @@ function formatArtifactLabel(artifact) {
357
398
  switch (artifact.kind) {
358
399
  case "download":
359
400
  if (artifact.exists !== true) {
360
- return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download event reported; file not verified" : "Download reported; file not verified";
401
+ return artifact.command === "wait" && isDownloadWaitSubcommand(artifact.subcommand) ? "Download event reported; file not verified" : "Download reported; file not verified";
361
402
  }
362
- return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download saved and verified" : "Downloaded file verified";
403
+ return artifact.command === "wait" && isDownloadWaitSubcommand(artifact.subcommand) ? "Download saved and verified" : "Downloaded file verified";
363
404
  case "file":
364
405
  return artifact.command === "state" ? "State file" : "Saved file";
365
406
  case "har":
@@ -375,11 +416,16 @@ function formatArtifactLabel(artifact) {
375
416
  case "trace":
376
417
  return "Saved trace";
377
418
  case "video":
378
- if (artifact.command === "record" && artifact.subcommand === "restart-previous")
419
+ if (artifact.command === "record" && artifact.subcommand === "restart-previous") {
420
+ if (artifact.status === "stale")
421
+ return "Previous recording stale";
422
+ if (artifact.exists === false)
423
+ return "Previous recording missing";
379
424
  return "Previous recording saved";
425
+ }
380
426
  if (!isPendingRecordingArtifact(artifact))
381
427
  return "Saved recording";
382
- return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started; output will be written on stop";
428
+ return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started in a fresh active page; output will be written on stop";
383
429
  }
384
430
  }
385
431
  export function formatArtifactSummary(artifacts) {
@@ -409,6 +455,7 @@ export function formatArtifactMetadataLines(artifacts) {
409
455
  `Status: ${artifact.status ?? "pending"}`,
410
456
  `Recording state: ${artifact.recordingState ?? "openRecording"}`,
411
457
  `Will exist on stop: ${artifact.willExistOnStop !== false}`,
458
+ artifact.subcommand === "start" ? "Page state: record start uses a fresh active page for video capture; prior in-page DOM and JavaScript state does not carry over. Take a fresh snapshot before continuing." : undefined,
412
459
  artifact.session ? `Session: ${artifact.session}` : undefined,
413
460
  artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
414
461
  `Machine data: details.artifacts[${index}]`,
@@ -433,7 +480,7 @@ export function formatArtifactMetadataLines(artifacts) {
433
480
  });
434
481
  }
435
482
  function isDownloadWaitCommand(commandInfo) {
436
- return commandInfo.command === "wait" && commandInfo.subcommand === "--download";
483
+ return commandInfo.command === "wait" && isDownloadWaitSubcommand(commandInfo.subcommand);
437
484
  }
438
485
  function extractSavedFilePath(data) {
439
486
  return typeof data.path === "string" && data.path.trim().length > 0 ? data.path : undefined;