@midscene/shared 1.12.1 → 1.12.2-beta-20260827052510.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.
@@ -14,28 +14,54 @@ function registerCliInterruptWaiter(source) {
14
14
  function hasActiveCliInterruptWaiter(source = process) {
15
15
  return (activeInterruptWaiters.get(source) ?? 0) > 0;
16
16
  }
17
- function waitForCliInterrupt(watchdogMs, source = process) {
17
+ function createCliInterruptWaiter(watchdogMs, source = process) {
18
18
  const unregisterWaiter = registerCliInterruptWaiter(source);
19
- return new Promise((resolve, reject)=>{
20
- let timer;
21
- let finished = false;
22
- const finish = (reason)=>{
23
- if (finished) return;
24
- finished = true;
25
- source.removeListener('SIGINT', onSigint);
26
- if (timer) clearTimeout(timer);
27
- unregisterWaiter();
28
- resolve(reason);
29
- };
30
- const onSigint = ()=>finish('sigint');
31
- try {
32
- source.once('SIGINT', onSigint);
33
- } catch (error) {
34
- unregisterWaiter();
35
- reject(error);
36
- return;
19
+ let timer;
20
+ let finished = false;
21
+ let disposed = false;
22
+ let resolveResult;
23
+ let rejectResult;
24
+ const result = new Promise((resolve, reject)=>{
25
+ resolveResult = resolve;
26
+ rejectResult = reject;
27
+ });
28
+ const finish = (reason)=>{
29
+ if (finished) return;
30
+ finished = true;
31
+ if (timer) {
32
+ clearTimeout(timer);
33
+ timer = void 0;
37
34
  }
35
+ resolveResult(reason);
36
+ };
37
+ const onSigint = ()=>finish('sigint');
38
+ const onSigterm = ()=>finish('sigterm');
39
+ const dispose = ()=>{
40
+ if (disposed) return;
41
+ disposed = true;
42
+ source.removeListener('SIGINT', onSigint);
43
+ source.removeListener('SIGTERM', onSigterm);
44
+ if (timer) {
45
+ clearTimeout(timer);
46
+ timer = void 0;
47
+ }
48
+ unregisterWaiter();
49
+ };
50
+ try {
51
+ source.on('SIGINT', onSigint);
52
+ source.on('SIGTERM', onSigterm);
38
53
  if (watchdogMs > 0) timer = setTimeout(()=>finish('watchdog'), watchdogMs);
39
- });
54
+ } catch (error) {
55
+ dispose();
56
+ rejectResult(error);
57
+ }
58
+ return {
59
+ result,
60
+ dispose
61
+ };
62
+ }
63
+ function waitForCliInterrupt(watchdogMs, source = process) {
64
+ const waiter = createCliInterruptWaiter(watchdogMs, source);
65
+ return waiter.result.finally(waiter.dispose);
40
66
  }
41
- export { hasActiveCliInterruptWaiter, waitForCliInterrupt };
67
+ export { createCliInterruptWaiter, hasActiveCliInterruptWaiter, waitForCliInterrupt };
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { getErrorMessage } from "../agent-tools/error-formatter.mjs";
3
3
  import { resolveObservationArtifactAdapter } from "../agent-tools/observation-artifact.mjs";
4
4
  import { writeUIObservationRecord } from "../agent-tools/observation-record.mjs";
5
- import { waitForCliInterrupt } from "./interrupt.mjs";
5
+ import { createCliInterruptWaiter } from "./interrupt.mjs";
6
6
  import { attachCliVerboseDumpListener, emitCliVerboseEvent } from "./verbose.mjs";
7
7
  const recordCliMetadata = {
8
8
  positionals: [
@@ -55,7 +55,7 @@ function createErrorResult(message) {
55
55
  function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata) {
56
56
  return {
57
57
  name: 'record',
58
- description: 'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
58
+ description: 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
59
59
  schema: {
60
60
  action: z.literal('start').describe('Start a foreground recording. Press Ctrl+C to finish.'),
61
61
  output: z.string().optional().describe('Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.'),
@@ -80,6 +80,7 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
80
80
  toolName: 'record'
81
81
  });
82
82
  let observer;
83
+ let interruptWaiter;
83
84
  try {
84
85
  const watchdogMs = args.watchdogMs ?? 300000;
85
86
  observer = await agent.startObserving({
@@ -87,12 +88,13 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
87
88
  maxFrames: args.maxFrames,
88
89
  watchdogMs
89
90
  });
91
+ interruptWaiter = createCliInterruptWaiter(watchdogMs);
90
92
  emitCliVerboseEvent({
91
93
  event: 'recording_ready',
92
94
  tool: 'record',
93
95
  watchdogMs
94
96
  });
95
- const stopReason = await waitForCliInterrupt(watchdogMs);
97
+ const stopReason = await interruptWaiter.result;
96
98
  emitCliVerboseEvent({
97
99
  event: 'recording_stopping',
98
100
  tool: 'record',
@@ -110,8 +112,12 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
110
112
  ]
111
113
  };
112
114
  } finally{
113
- await observer?.dispose?.();
114
- unsubscribeVerbose();
115
+ try {
116
+ await observer?.dispose?.();
117
+ } finally{
118
+ interruptWaiter?.dispose();
119
+ unsubscribeVerbose();
120
+ }
115
121
  }
116
122
  } catch (error) {
117
123
  const errorMessage = getErrorMessage(error);
@@ -305,7 +305,8 @@ function renderCliVerboseEventText(event, context) {
305
305
  case 'recording_ready':
306
306
  return '[Midscene] Recording. Press Ctrl+C to stop and save.';
307
307
  case 'recording_stopping':
308
- return 'watchdog' === event.reason ? '[Midscene] Recording watchdog reached; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
308
+ if ('watchdog' === event.reason) return '[Midscene] Recording watchdog reached; finalizing and saving.';
309
+ return 'sigterm' === event.reason ? '[Midscene] SIGTERM received; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
309
310
  case 'dump_update':
310
311
  {
311
312
  if (isActVerboseEvent(command, tool)) return;
@@ -5,7 +5,7 @@ import { assert } from "../utils.mjs";
5
5
  import { maskConfig, parseJson } from "./helper.mjs";
6
6
  import { initDebugConfig } from "./init-debug.mjs";
7
7
  const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
8
- const getCurrentVersion = ()=>"1.12.1";
8
+ const getCurrentVersion = ()=>"1.12.2-beta-20260827052510.0";
9
9
  const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
10
10
  const KEYS_MAP = {
11
11
  insight: INSIGHT_MODEL_CONFIG_KEYS,
@@ -24,6 +24,7 @@ var __webpack_require__ = {};
24
24
  var __webpack_exports__ = {};
25
25
  __webpack_require__.r(__webpack_exports__);
26
26
  __webpack_require__.d(__webpack_exports__, {
27
+ createCliInterruptWaiter: ()=>createCliInterruptWaiter,
27
28
  hasActiveCliInterruptWaiter: ()=>hasActiveCliInterruptWaiter,
28
29
  waitForCliInterrupt: ()=>waitForCliInterrupt
29
30
  });
@@ -43,33 +44,61 @@ function registerCliInterruptWaiter(source) {
43
44
  function hasActiveCliInterruptWaiter(source = process) {
44
45
  return (activeInterruptWaiters.get(source) ?? 0) > 0;
45
46
  }
46
- function waitForCliInterrupt(watchdogMs, source = process) {
47
+ function createCliInterruptWaiter(watchdogMs, source = process) {
47
48
  const unregisterWaiter = registerCliInterruptWaiter(source);
48
- return new Promise((resolve, reject)=>{
49
- let timer;
50
- let finished = false;
51
- const finish = (reason)=>{
52
- if (finished) return;
53
- finished = true;
54
- source.removeListener('SIGINT', onSigint);
55
- if (timer) clearTimeout(timer);
56
- unregisterWaiter();
57
- resolve(reason);
58
- };
59
- const onSigint = ()=>finish('sigint');
60
- try {
61
- source.once('SIGINT', onSigint);
62
- } catch (error) {
63
- unregisterWaiter();
64
- reject(error);
65
- return;
49
+ let timer;
50
+ let finished = false;
51
+ let disposed = false;
52
+ let resolveResult;
53
+ let rejectResult;
54
+ const result = new Promise((resolve, reject)=>{
55
+ resolveResult = resolve;
56
+ rejectResult = reject;
57
+ });
58
+ const finish = (reason)=>{
59
+ if (finished) return;
60
+ finished = true;
61
+ if (timer) {
62
+ clearTimeout(timer);
63
+ timer = void 0;
66
64
  }
65
+ resolveResult(reason);
66
+ };
67
+ const onSigint = ()=>finish('sigint');
68
+ const onSigterm = ()=>finish('sigterm');
69
+ const dispose = ()=>{
70
+ if (disposed) return;
71
+ disposed = true;
72
+ source.removeListener('SIGINT', onSigint);
73
+ source.removeListener('SIGTERM', onSigterm);
74
+ if (timer) {
75
+ clearTimeout(timer);
76
+ timer = void 0;
77
+ }
78
+ unregisterWaiter();
79
+ };
80
+ try {
81
+ source.on('SIGINT', onSigint);
82
+ source.on('SIGTERM', onSigterm);
67
83
  if (watchdogMs > 0) timer = setTimeout(()=>finish('watchdog'), watchdogMs);
68
- });
84
+ } catch (error) {
85
+ dispose();
86
+ rejectResult(error);
87
+ }
88
+ return {
89
+ result,
90
+ dispose
91
+ };
92
+ }
93
+ function waitForCliInterrupt(watchdogMs, source = process) {
94
+ const waiter = createCliInterruptWaiter(watchdogMs, source);
95
+ return waiter.result.finally(waiter.dispose);
69
96
  }
97
+ exports.createCliInterruptWaiter = __webpack_exports__.createCliInterruptWaiter;
70
98
  exports.hasActiveCliInterruptWaiter = __webpack_exports__.hasActiveCliInterruptWaiter;
71
99
  exports.waitForCliInterrupt = __webpack_exports__.waitForCliInterrupt;
72
100
  for(var __rspack_i in __webpack_exports__)if (-1 === [
101
+ "createCliInterruptWaiter",
73
102
  "hasActiveCliInterruptWaiter",
74
103
  "waitForCliInterrupt"
75
104
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -83,7 +83,7 @@ function createErrorResult(message) {
83
83
  function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata) {
84
84
  return {
85
85
  name: 'record',
86
- description: 'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
86
+ description: 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
87
87
  schema: {
88
88
  action: external_zod_namespaceObject.z.literal('start').describe('Start a foreground recording. Press Ctrl+C to finish.'),
89
89
  output: external_zod_namespaceObject.z.string().optional().describe('Path for the JSON observation manifest. Frames are stored in an adjacent <name>.frames directory. Defaults to a generated path under midscene_run/output.'),
@@ -108,6 +108,7 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
108
108
  toolName: 'record'
109
109
  });
110
110
  let observer;
111
+ let interruptWaiter;
111
112
  try {
112
113
  const watchdogMs = args.watchdogMs ?? 300000;
113
114
  observer = await agent.startObserving({
@@ -115,12 +116,13 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
115
116
  maxFrames: args.maxFrames,
116
117
  watchdogMs
117
118
  });
119
+ interruptWaiter = (0, external_interrupt_js_namespaceObject.createCliInterruptWaiter)(watchdogMs);
118
120
  (0, external_verbose_js_namespaceObject.emitCliVerboseEvent)({
119
121
  event: 'recording_ready',
120
122
  tool: 'record',
121
123
  watchdogMs
122
124
  });
123
- const stopReason = await (0, external_interrupt_js_namespaceObject.waitForCliInterrupt)(watchdogMs);
125
+ const stopReason = await interruptWaiter.result;
124
126
  (0, external_verbose_js_namespaceObject.emitCliVerboseEvent)({
125
127
  event: 'recording_stopping',
126
128
  tool: 'record',
@@ -138,8 +140,12 @@ function createRecordCliCommand(getAgent, initArgSchema = {}, initArgCliMetadata
138
140
  ]
139
141
  };
140
142
  } finally{
141
- await observer?.dispose?.();
142
- unsubscribeVerbose();
143
+ try {
144
+ await observer?.dispose?.();
145
+ } finally{
146
+ interruptWaiter?.dispose();
147
+ unsubscribeVerbose();
148
+ }
143
149
  }
144
150
  } catch (error) {
145
151
  const errorMessage = (0, error_formatter_js_namespaceObject.getErrorMessage)(error);
@@ -342,7 +342,8 @@ function renderCliVerboseEventText(event, context) {
342
342
  case 'recording_ready':
343
343
  return '[Midscene] Recording. Press Ctrl+C to stop and save.';
344
344
  case 'recording_stopping':
345
- return 'watchdog' === event.reason ? '[Midscene] Recording watchdog reached; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
345
+ if ('watchdog' === event.reason) return '[Midscene] Recording watchdog reached; finalizing and saving.';
346
+ return 'sigterm' === event.reason ? '[Midscene] SIGTERM received; finalizing and saving.' : '[Midscene] Ctrl+C received; finalizing and saving.';
346
347
  case 'dump_update':
347
348
  {
348
349
  if (isActVerboseEvent(command, tool)) return;
@@ -37,7 +37,7 @@ const external_utils_js_namespaceObject = require("../utils.js");
37
37
  const external_helper_js_namespaceObject = require("./helper.js");
38
38
  const external_init_debug_js_namespaceObject = require("./init-debug.js");
39
39
  const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
40
- const getCurrentVersion = ()=>"1.12.1";
40
+ const getCurrentVersion = ()=>"1.12.2-beta-20260827052510.0";
41
41
  const getInvalidModelFamilyMessage = (modelFamily)=>`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${external_types_js_namespaceObject.MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
42
42
  const KEYS_MAP = {
43
43
  insight: external_constants_js_namespaceObject.INSIGHT_MODEL_CONFIG_KEYS,
@@ -1,13 +1,30 @@
1
- export type CliInterruptReason = 'sigint' | 'watchdog';
1
+ export type CliInterruptReason = 'sigint' | 'sigterm' | 'watchdog';
2
+ type CliInterruptSignal = 'SIGINT' | 'SIGTERM';
2
3
  export interface CliInterruptSource {
3
- once(event: 'SIGINT', listener: () => void): unknown;
4
- removeListener(event: 'SIGINT', listener: () => void): unknown;
4
+ on(event: CliInterruptSignal, listener: () => void): unknown;
5
+ removeListener(event: CliInterruptSignal, listener: () => void): unknown;
6
+ }
7
+ export interface CliInterruptWaiter {
8
+ /** Resolves on the first stop signal or watchdog timeout. */
9
+ readonly result: Promise<CliInterruptReason>;
10
+ /** Release signal handlers after asynchronous finalization has completed. */
11
+ dispose(): void;
5
12
  }
6
13
  /** Whether a foreground CLI command is currently waiting for this source. */
7
14
  export declare function hasActiveCliInterruptWaiter(source?: CliInterruptSource): boolean;
8
15
  /**
9
- * Wait until the foreground CLI receives Ctrl+C. A positive watchdog keeps a
10
- * forgotten recording from running forever and uses the same graceful save
11
- * path as an explicit interrupt.
16
+ * Keep graceful-stop handlers installed until the caller has finished saving.
17
+ *
18
+ * Package runners such as pnpm can deliver SIGINT to the foreground child and
19
+ * immediately follow it with SIGTERM while shutting down their own process.
20
+ * Resolving on the first signal is not enough: removing the handlers at that
21
+ * point lets the forwarded SIGTERM kill the child during asynchronous artifact
22
+ * finalization.
23
+ */
24
+ export declare function createCliInterruptWaiter(watchdogMs: number, source?: CliInterruptSource): CliInterruptWaiter;
25
+ /**
26
+ * Wait for one stop request and release the handlers immediately afterwards.
27
+ * Long-running finalizers should use {@link createCliInterruptWaiter} instead.
12
28
  */
13
29
  export declare function waitForCliInterrupt(watchdogMs: number, source?: CliInterruptSource): Promise<CliInterruptReason>;
30
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/shared",
3
- "version": "1.12.1",
3
+ "version": "1.12.2-beta-20260827052510.0",
4
4
  "repository": "https://github.com/web-infra-dev/midscene",
5
5
  "homepage": "https://midscenejs.com/",
6
6
  "types": "./dist/types/index.d.ts",
@@ -1,8 +1,17 @@
1
- export type CliInterruptReason = 'sigint' | 'watchdog';
1
+ export type CliInterruptReason = 'sigint' | 'sigterm' | 'watchdog';
2
+
3
+ type CliInterruptSignal = 'SIGINT' | 'SIGTERM';
2
4
 
3
5
  export interface CliInterruptSource {
4
- once(event: 'SIGINT', listener: () => void): unknown;
5
- removeListener(event: 'SIGINT', listener: () => void): unknown;
6
+ on(event: CliInterruptSignal, listener: () => void): unknown;
7
+ removeListener(event: CliInterruptSignal, listener: () => void): unknown;
8
+ }
9
+
10
+ export interface CliInterruptWaiter {
11
+ /** Resolves on the first stop signal or watchdog timeout. */
12
+ readonly result: Promise<CliInterruptReason>;
13
+ /** Release signal handlers after asynchronous finalization has completed. */
14
+ dispose(): void;
6
15
  }
7
16
 
8
17
  const activeInterruptWaiters = new WeakMap<object, number>();
@@ -33,39 +42,76 @@ export function hasActiveCliInterruptWaiter(
33
42
  }
34
43
 
35
44
  /**
36
- * Wait until the foreground CLI receives Ctrl+C. A positive watchdog keeps a
37
- * forgotten recording from running forever and uses the same graceful save
38
- * path as an explicit interrupt.
45
+ * Keep graceful-stop handlers installed until the caller has finished saving.
46
+ *
47
+ * Package runners such as pnpm can deliver SIGINT to the foreground child and
48
+ * immediately follow it with SIGTERM while shutting down their own process.
49
+ * Resolving on the first signal is not enough: removing the handlers at that
50
+ * point lets the forwarded SIGTERM kill the child during asynchronous artifact
51
+ * finalization.
39
52
  */
40
- export function waitForCliInterrupt(
53
+ export function createCliInterruptWaiter(
41
54
  watchdogMs: number,
42
55
  source: CliInterruptSource = process,
43
- ): Promise<CliInterruptReason> {
56
+ ): CliInterruptWaiter {
44
57
  const unregisterWaiter = registerCliInterruptWaiter(source);
58
+ let timer: ReturnType<typeof setTimeout> | undefined;
59
+ let finished = false;
60
+ let disposed = false;
61
+ let resolveResult!: (reason: CliInterruptReason) => void;
62
+ let rejectResult!: (error: unknown) => void;
45
63
 
46
- return new Promise((resolve, reject) => {
47
- let timer: ReturnType<typeof setTimeout> | undefined;
48
- let finished = false;
64
+ const result = new Promise<CliInterruptReason>((resolve, reject) => {
65
+ resolveResult = resolve;
66
+ rejectResult = reject;
67
+ });
49
68
 
50
- const finish = (reason: CliInterruptReason) => {
51
- if (finished) return;
52
- finished = true;
53
- source.removeListener('SIGINT', onSigint);
54
- if (timer) clearTimeout(timer);
55
- unregisterWaiter();
56
- resolve(reason);
57
- };
58
- const onSigint = () => finish('sigint');
69
+ const finish = (reason: CliInterruptReason) => {
70
+ if (finished) return;
71
+ finished = true;
72
+ if (timer) {
73
+ clearTimeout(timer);
74
+ timer = undefined;
75
+ }
76
+ resolveResult(reason);
77
+ };
78
+ const onSigint = () => finish('sigint');
79
+ const onSigterm = () => finish('sigterm');
59
80
 
60
- try {
61
- source.once('SIGINT', onSigint);
62
- } catch (error) {
63
- unregisterWaiter();
64
- reject(error);
65
- return;
81
+ const dispose = () => {
82
+ if (disposed) return;
83
+ disposed = true;
84
+ source.removeListener('SIGINT', onSigint);
85
+ source.removeListener('SIGTERM', onSigterm);
86
+ if (timer) {
87
+ clearTimeout(timer);
88
+ timer = undefined;
66
89
  }
90
+ unregisterWaiter();
91
+ };
92
+
93
+ try {
94
+ source.on('SIGINT', onSigint);
95
+ source.on('SIGTERM', onSigterm);
67
96
  if (watchdogMs > 0) {
68
97
  timer = setTimeout(() => finish('watchdog'), watchdogMs);
69
98
  }
70
- });
99
+ } catch (error) {
100
+ dispose();
101
+ rejectResult(error);
102
+ }
103
+
104
+ return { result, dispose };
105
+ }
106
+
107
+ /**
108
+ * Wait for one stop request and release the handlers immediately afterwards.
109
+ * Long-running finalizers should use {@link createCliInterruptWaiter} instead.
110
+ */
111
+ export function waitForCliInterrupt(
112
+ watchdogMs: number,
113
+ source: CliInterruptSource = process,
114
+ ): Promise<CliInterruptReason> {
115
+ const waiter = createCliInterruptWaiter(watchdogMs, source);
116
+ return waiter.result.finally(waiter.dispose);
71
117
  }
@@ -9,7 +9,7 @@ import type {
9
9
  ToolResult,
10
10
  ToolSchema,
11
11
  } from '../agent-tools/types';
12
- import { waitForCliInterrupt } from './interrupt';
12
+ import { type CliInterruptWaiter, createCliInterruptWaiter } from './interrupt';
13
13
  import { attachCliVerboseDumpListener, emitCliVerboseEvent } from './verbose';
14
14
 
15
15
  const recordCliMetadata: ToolCliMetadata = {
@@ -62,7 +62,7 @@ export function createRecordCliCommand(
62
62
  return {
63
63
  name: 'record',
64
64
  description:
65
- 'Record the page/screen in the foreground until Ctrl+C, then save the ordered frame window for a later assert command.',
65
+ 'Record the page/screen in the foreground until Ctrl+C or a termination signal, then save the ordered frame window for a later assert command.',
66
66
  schema: {
67
67
  action: z
68
68
  .literal('start')
@@ -125,6 +125,7 @@ export function createRecordCliCommand(
125
125
  let observer:
126
126
  | Awaited<ReturnType<NonNullable<BaseAgent['startObserving']>>>
127
127
  | undefined;
128
+ let interruptWaiter: CliInterruptWaiter | undefined;
128
129
  try {
129
130
  const watchdogMs = (args.watchdogMs as number | undefined) ?? 300_000;
130
131
  observer = await agent.startObserving({
@@ -132,12 +133,13 @@ export function createRecordCliCommand(
132
133
  maxFrames: args.maxFrames as number | undefined,
133
134
  watchdogMs,
134
135
  });
136
+ interruptWaiter = createCliInterruptWaiter(watchdogMs);
135
137
  emitCliVerboseEvent({
136
138
  event: 'recording_ready',
137
139
  tool: 'record',
138
140
  watchdogMs,
139
141
  });
140
- const stopReason = await waitForCliInterrupt(watchdogMs);
142
+ const stopReason = await interruptWaiter.result;
141
143
  emitCliVerboseEvent({
142
144
  event: 'recording_stopping',
143
145
  tool: 'record',
@@ -158,8 +160,12 @@ export function createRecordCliCommand(
158
160
  ],
159
161
  };
160
162
  } finally {
161
- await observer?.dispose?.();
162
- unsubscribeVerbose();
163
+ try {
164
+ await observer?.dispose?.();
165
+ } finally {
166
+ interruptWaiter?.dispose();
167
+ unsubscribeVerbose();
168
+ }
163
169
  }
164
170
  } catch (error: unknown) {
165
171
  const errorMessage = getErrorMessage(error);
@@ -587,8 +587,11 @@ function renderCliVerboseEventText(
587
587
  case 'recording_ready':
588
588
  return '[Midscene] Recording. Press Ctrl+C to stop and save.';
589
589
  case 'recording_stopping':
590
- return event.reason === 'watchdog'
591
- ? '[Midscene] Recording watchdog reached; finalizing and saving.'
590
+ if (event.reason === 'watchdog') {
591
+ return '[Midscene] Recording watchdog reached; finalizing and saving.';
592
+ }
593
+ return event.reason === 'sigterm'
594
+ ? '[Midscene] SIGTERM received; finalizing and saving.'
592
595
  : '[Midscene] Ctrl+C received; finalizing and saving.';
593
596
  case 'dump_update': {
594
597
  if (isActVerboseEvent(command, tool)) {