@playcraft/cli 0.0.59 → 0.1.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.
- package/dist/agent/local-backend.js +1 -1
- package/dist/workspace-runtime/codex/app-server.js +46 -121
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +19 -2
- package/dist/workspace-runtime/codex/loopback.js +90 -39
- package/dist/workspace-runtime/codex/native-adapter.js +3 -0
- package/dist/workspace-runtime/persistence/journal.js +213 -200
- package/dist/workspace-runtime/persistence/sqlite-client.js +61 -0
- package/dist/workspace-runtime/persistence/sqlite-worker.js +1025 -0
- package/dist/workspace-runtime/serve.js +27 -1
- package/dist/workspace-runtime/server/desktop-host.js +314 -0
- package/dist/workspace-runtime/server/dispatch.js +107 -48
- package/dist/workspace-runtime/server/http.js +196 -11
- package/dist/workspace-runtime/server/terminal-host.js +243 -0
- package/package.json +6 -3
|
@@ -2,7 +2,7 @@ import express from 'express';
|
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
|
-
import { eq, inArray } from '
|
|
5
|
+
import { eq, inArray } from '@playcraft/common/database/operators';
|
|
6
6
|
import pc from 'picocolors';
|
|
7
7
|
import { createSqliteAdapter } from '@playcraft/common/database';
|
|
8
8
|
import { AssetModel, ProjectModel, SceneModel } from '@playcraft/common/models';
|
|
@@ -3,17 +3,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { writePlaycraftCodexConfig } from './config-toml.js';
|
|
5
5
|
import { createJsonRpcStdioClient } from './jsonrpc-stdio.js';
|
|
6
|
-
import {
|
|
6
|
+
import { collectedEvent } from './native-adapter.js';
|
|
7
7
|
function asRecord(value) {
|
|
8
8
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
9
9
|
? value
|
|
10
10
|
: {};
|
|
11
11
|
}
|
|
12
|
-
function nestedId(value, key) {
|
|
13
|
-
const record = asRecord(value);
|
|
14
|
-
const nested = asRecord(record[key]);
|
|
15
|
-
return typeof nested.id === 'string' ? nested.id : undefined;
|
|
16
|
-
}
|
|
17
12
|
function waitForChildExit(child) {
|
|
18
13
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
19
14
|
return Promise.resolve();
|
|
@@ -62,14 +57,10 @@ export function createCodexAppServerAdapter(options) {
|
|
|
62
57
|
let startPromise;
|
|
63
58
|
let live;
|
|
64
59
|
const exitListeners = new Set();
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
unmatchedTurns.delete(turnId);
|
|
70
|
-
const status = String(asRecord(asRecord(params).turn).status ?? 'completed');
|
|
71
|
-
const outcome = status === 'interrupted' ? 'interrupted' : status === 'failed' ? 'failed' : 'completed';
|
|
72
|
-
waiter.resolve({ outcome, turnId, events: waiter.events });
|
|
60
|
+
const collected = new Set();
|
|
61
|
+
function emitCollected(event) {
|
|
62
|
+
for (const listener of collected)
|
|
63
|
+
listener(event);
|
|
73
64
|
}
|
|
74
65
|
async function ensureStarted() {
|
|
75
66
|
if (readyFlag && rpc)
|
|
@@ -103,20 +94,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
103
94
|
});
|
|
104
95
|
rpc = createJsonRpcStdioClient(child);
|
|
105
96
|
rpc.onNotification((method, params) => {
|
|
106
|
-
|
|
107
|
-
const event = nativeEvent(method, params);
|
|
108
|
-
if (!turnId)
|
|
109
|
-
return;
|
|
110
|
-
const waiter = turnWaiters.get(turnId);
|
|
111
|
-
if (waiter) {
|
|
112
|
-
waiter.events.push(event);
|
|
113
|
-
if (method === 'turn/completed')
|
|
114
|
-
finishTurn(turnId, params, waiter);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
const queued = unmatchedTurns.get(turnId) ?? [];
|
|
118
|
-
queued.push(event);
|
|
119
|
-
unmatchedTurns.set(turnId, queued);
|
|
97
|
+
emitCollected(collectedEvent('notification', method, params));
|
|
120
98
|
});
|
|
121
99
|
rpc.onServerRequest((id, method, params) => {
|
|
122
100
|
const payload = asRecord(params);
|
|
@@ -127,19 +105,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
127
105
|
threadId: String(payload.threadId ?? ''),
|
|
128
106
|
turnId: turnId || undefined,
|
|
129
107
|
});
|
|
130
|
-
|
|
131
|
-
const waiter = turnId ? turnWaiters.get(turnId) : undefined;
|
|
132
|
-
if (waiter) {
|
|
133
|
-
waiter.events.push(event);
|
|
134
|
-
turnWaiters.delete(turnId);
|
|
135
|
-
waiter.resolve({ outcome: 'completed', turnId, approvalId, events: waiter.events });
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (turnId) {
|
|
139
|
-
const queued = unmatchedTurns.get(turnId) ?? [];
|
|
140
|
-
queued.push(event);
|
|
141
|
-
unmatchedTurns.set(turnId, queued);
|
|
142
|
-
}
|
|
108
|
+
emitCollected(collectedEvent('approval_request', method, { ...payload, itemId: approvalId }));
|
|
143
109
|
});
|
|
144
110
|
const pid = child.pid;
|
|
145
111
|
if (!pid)
|
|
@@ -158,10 +124,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
158
124
|
live = undefined;
|
|
159
125
|
rpc = undefined;
|
|
160
126
|
child = undefined;
|
|
161
|
-
|
|
162
|
-
turnWaiters.delete(turnId);
|
|
163
|
-
waiter.resolve({ outcome: 'uncertain', turnId, events: waiter.events });
|
|
164
|
-
}
|
|
127
|
+
emitCollected(collectedEvent('process_exit', 'process.exited', { pid: current.pid }));
|
|
165
128
|
}
|
|
166
129
|
if (!current.superseded && !closed) {
|
|
167
130
|
for (const listener of exitListeners)
|
|
@@ -189,6 +152,13 @@ export function createCodexAppServerAdapter(options) {
|
|
|
189
152
|
onUnexpectedExit(listener) {
|
|
190
153
|
exitListeners.add(listener);
|
|
191
154
|
},
|
|
155
|
+
onCollected(listener) {
|
|
156
|
+
collected.add(listener);
|
|
157
|
+
return () => collected.delete(listener);
|
|
158
|
+
},
|
|
159
|
+
turnStartSends() {
|
|
160
|
+
return rpc?.turnStartSends() ?? 0;
|
|
161
|
+
},
|
|
192
162
|
async replaceProcess() {
|
|
193
163
|
const previous = live;
|
|
194
164
|
if (previous)
|
|
@@ -197,8 +167,6 @@ export function createCodexAppServerAdapter(options) {
|
|
|
197
167
|
startPromise = undefined;
|
|
198
168
|
threads.clear();
|
|
199
169
|
approvals.clear();
|
|
200
|
-
turnWaiters.clear();
|
|
201
|
-
unmatchedTurns.clear();
|
|
202
170
|
if (previous && previous.child.exitCode === null && previous.child.signalCode === null) {
|
|
203
171
|
previous.child.kill('SIGTERM');
|
|
204
172
|
}
|
|
@@ -265,104 +233,61 @@ export function createCodexAppServerAdapter(options) {
|
|
|
265
233
|
await ensureStarted();
|
|
266
234
|
if (!rpc)
|
|
267
235
|
throw new Error('codex_not_started');
|
|
268
|
-
if (input.simulateLostReceipt) {
|
|
269
|
-
return { outcome: 'uncertain', events: [] };
|
|
270
|
-
}
|
|
271
236
|
if (threads.get(input.threadId)?.sessionId !== input.sessionId) {
|
|
272
|
-
return {
|
|
273
|
-
outcome: 'failed',
|
|
274
|
-
events: [nativeEvent('turn.failed', { reason: 'unknown_thread' })],
|
|
275
|
-
};
|
|
237
|
+
return { outcome: 'failed', error: 'unknown_thread' };
|
|
276
238
|
}
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
thread
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const alreadyDone = queued.find((event) => event.type === 'turn/completed');
|
|
291
|
-
if (alreadyDone) {
|
|
292
|
-
const status = String(asRecord(asRecord(alreadyDone.payload).turn).status ?? 'completed');
|
|
293
|
-
const outcome = status === 'interrupted' ? 'interrupted' : status === 'failed' ? 'failed' : 'completed';
|
|
294
|
-
return { outcome, turnId, events };
|
|
239
|
+
const requestId = rpc.reserveRequest();
|
|
240
|
+
await input.persistReservation({ nativeRequestId: String(requestId) });
|
|
241
|
+
if (input.simulateLostReceipt)
|
|
242
|
+
return { outcome: 'uncertain', threadId: input.threadId };
|
|
243
|
+
try {
|
|
244
|
+
const started = await rpc.sendReserved(requestId, 'turn/start', { threadId: input.threadId, input: [{ type: 'text', text: input.text }] }, turnTimeoutMs);
|
|
245
|
+
const turnId = started.turn?.id;
|
|
246
|
+
if (!turnId)
|
|
247
|
+
return { outcome: 'uncertain', threadId: input.threadId };
|
|
248
|
+
const thread = threads.get(input.threadId);
|
|
249
|
+
if (thread)
|
|
250
|
+
thread.lastTurnId = turnId;
|
|
251
|
+
return { outcome: 'accepted', threadId: input.threadId, turnId };
|
|
295
252
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const approvalId = String(asRecord(approvalEvent.payload).itemId ?? '');
|
|
299
|
-
return { outcome: 'completed', turnId, approvalId, events };
|
|
253
|
+
catch {
|
|
254
|
+
return { outcome: 'uncertain', threadId: input.threadId };
|
|
300
255
|
}
|
|
301
|
-
return await new Promise((resolve) => {
|
|
302
|
-
let timer;
|
|
303
|
-
const waiter = {
|
|
304
|
-
events,
|
|
305
|
-
resolve: (result) => {
|
|
306
|
-
clearTimeout(timer);
|
|
307
|
-
resolve(result);
|
|
308
|
-
},
|
|
309
|
-
};
|
|
310
|
-
timer = setTimeout(() => {
|
|
311
|
-
turnWaiters.delete(turnId);
|
|
312
|
-
resolve({ outcome: 'uncertain', turnId, events: waiter.events });
|
|
313
|
-
}, turnTimeoutMs);
|
|
314
|
-
turnWaiters.set(turnId, waiter);
|
|
315
|
-
});
|
|
316
256
|
},
|
|
317
257
|
async interrupt(input) {
|
|
318
258
|
await ensureStarted();
|
|
319
259
|
if (!rpc)
|
|
320
|
-
return { outcome: 'uncertain'
|
|
260
|
+
return { outcome: 'uncertain' };
|
|
321
261
|
if (threads.get(input.threadId)?.sessionId !== input.sessionId)
|
|
322
|
-
return { outcome: 'uncertain'
|
|
262
|
+
return { outcome: 'uncertain' };
|
|
323
263
|
const turnId = threads.get(input.threadId)?.lastTurnId;
|
|
324
264
|
if (!turnId)
|
|
325
|
-
return { outcome: 'uncertain'
|
|
265
|
+
return { outcome: 'uncertain' };
|
|
326
266
|
try {
|
|
327
267
|
await rpc.request('turn/interrupt', { threadId: input.threadId, turnId });
|
|
328
268
|
}
|
|
329
269
|
catch {
|
|
330
|
-
return { outcome: 'uncertain',
|
|
270
|
+
return { outcome: 'uncertain', turnId };
|
|
331
271
|
}
|
|
332
|
-
return {
|
|
333
|
-
outcome: 'interrupted',
|
|
334
|
-
turnId,
|
|
335
|
-
events: [nativeEvent('turn.interrupted', { threadId: input.threadId, turnId })],
|
|
336
|
-
};
|
|
272
|
+
return { outcome: 'accepted', turnId };
|
|
337
273
|
},
|
|
338
274
|
async respondApproval(input) {
|
|
339
|
-
if (!rpc)
|
|
340
|
-
return {
|
|
341
|
-
outcome: 'failed',
|
|
342
|
-
events: [nativeEvent('approval.unknown', { approvalId: input.approvalId })],
|
|
343
|
-
};
|
|
344
|
-
}
|
|
275
|
+
if (!rpc)
|
|
276
|
+
return { outcome: 'failed', approvalId: input.approvalId };
|
|
345
277
|
const threadId = [...threads].find(([, thread]) => thread.sessionId === input.sessionId)?.[0];
|
|
346
278
|
const key = JSON.stringify([threadId, input.approvalId]);
|
|
347
279
|
const pending = approvals.get(key);
|
|
348
|
-
if (!pending)
|
|
349
|
-
return {
|
|
350
|
-
outcome: 'failed',
|
|
351
|
-
events: [nativeEvent('approval.unknown', { approvalId: input.approvalId })],
|
|
352
|
-
};
|
|
353
|
-
}
|
|
280
|
+
if (!pending)
|
|
281
|
+
return { outcome: 'failed', approvalId: input.approvalId };
|
|
354
282
|
approvals.delete(key);
|
|
355
283
|
rpc.respond(pending.rpcId, { decision: input.accepted ? 'accept' : 'decline' });
|
|
356
|
-
|
|
357
|
-
outcome: 'completed',
|
|
284
|
+
emitCollected(collectedEvent('notification', 'approval.resolved', {
|
|
358
285
|
approvalId: input.approvalId,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
],
|
|
365
|
-
};
|
|
286
|
+
accepted: input.accepted,
|
|
287
|
+
threadId,
|
|
288
|
+
turnId: pending.turnId,
|
|
289
|
+
}));
|
|
290
|
+
return { outcome: 'accepted', approvalId: input.approvalId, turnId: pending.turnId };
|
|
366
291
|
},
|
|
367
292
|
async close() {
|
|
368
293
|
if (closed)
|
|
@@ -7,6 +7,7 @@ export function createJsonRpcStdioClient(child) {
|
|
|
7
7
|
const emitter = new EventEmitter();
|
|
8
8
|
const pending = new Map();
|
|
9
9
|
let nextId = 1;
|
|
10
|
+
let turnStarts = 0;
|
|
10
11
|
let buffer = '';
|
|
11
12
|
let closed = false;
|
|
12
13
|
const onData = (chunk) => {
|
|
@@ -60,8 +61,16 @@ export function createJsonRpcStdioClient(child) {
|
|
|
60
61
|
notify(method, params) {
|
|
61
62
|
send({ method, params: params ?? {} });
|
|
62
63
|
},
|
|
63
|
-
|
|
64
|
-
const id = nextId
|
|
64
|
+
reserveRequest() {
|
|
65
|
+
const id = nextId;
|
|
66
|
+
nextId += 1;
|
|
67
|
+
return id;
|
|
68
|
+
},
|
|
69
|
+
sendReserved(id, method, params, timeoutMs = 20_000) {
|
|
70
|
+
if (pending.has(id))
|
|
71
|
+
throw new Error('jsonrpc_request_in_use');
|
|
72
|
+
if (method === 'turn/start')
|
|
73
|
+
turnStarts += 1;
|
|
65
74
|
return new Promise((resolve, reject) => {
|
|
66
75
|
const timer = setTimeout(() => {
|
|
67
76
|
pending.delete(id);
|
|
@@ -78,6 +87,14 @@ export function createJsonRpcStdioClient(child) {
|
|
|
78
87
|
}
|
|
79
88
|
});
|
|
80
89
|
},
|
|
90
|
+
request(method, params, timeoutMs = 20_000) {
|
|
91
|
+
const id = nextId;
|
|
92
|
+
nextId += 1;
|
|
93
|
+
return this.sendReserved(id, method, params, timeoutMs);
|
|
94
|
+
},
|
|
95
|
+
turnStartSends() {
|
|
96
|
+
return turnStarts;
|
|
97
|
+
},
|
|
81
98
|
onNotification(listener) {
|
|
82
99
|
emitter.on('notification', listener);
|
|
83
100
|
return () => emitter.off('notification', listener);
|
|
@@ -1,59 +1,110 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectedEvent } from './native-adapter.js';
|
|
2
2
|
/** In-process Codex stand-in. Not the App Server; used to prove the operation/event loop. */
|
|
3
3
|
export function createLoopbackNativeAdapter() {
|
|
4
4
|
const threads = new Map();
|
|
5
5
|
const approvals = new Set();
|
|
6
|
+
const collected = new Set();
|
|
7
|
+
const sent = new Set();
|
|
8
|
+
let turnStarts = 0;
|
|
9
|
+
function emit(event) {
|
|
10
|
+
for (const listener of collected)
|
|
11
|
+
listener(event);
|
|
12
|
+
}
|
|
6
13
|
return {
|
|
7
14
|
name: 'loopback',
|
|
8
15
|
ready: () => true,
|
|
16
|
+
onCollected(listener) {
|
|
17
|
+
collected.add(listener);
|
|
18
|
+
return () => collected.delete(listener);
|
|
19
|
+
},
|
|
20
|
+
turnStartSends() {
|
|
21
|
+
return turnStarts;
|
|
22
|
+
},
|
|
9
23
|
async startThread(input) {
|
|
10
24
|
const threadId = `thread_${input.sessionId}`;
|
|
11
|
-
threads.set(threadId, input.sessionId);
|
|
25
|
+
threads.set(threadId, { sessionId: input.sessionId });
|
|
12
26
|
return { threadId };
|
|
13
27
|
},
|
|
14
28
|
async startTurn(input) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
if (input.simulateLostReceipt) {
|
|
22
|
-
|
|
29
|
+
const thread = threads.get(input.threadId);
|
|
30
|
+
if (!thread || thread.sessionId !== input.sessionId)
|
|
31
|
+
return { outcome: 'failed', error: 'unknown_thread' };
|
|
32
|
+
const nativeRequestId = `loopback:${input.sessionId}:${input.clientRequestId}`;
|
|
33
|
+
const sentKey = `${input.sessionId}:${input.clientRequestId}`;
|
|
34
|
+
await input.persistReservation({ nativeRequestId });
|
|
35
|
+
if (input.simulateLostReceipt || sent.has(sentKey)) {
|
|
36
|
+
sent.add(sentKey);
|
|
37
|
+
return { outcome: 'uncertain', threadId: input.threadId };
|
|
23
38
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
sent.add(sentKey);
|
|
40
|
+
turnStarts += 1;
|
|
41
|
+
const turnId = `turn_${input.sessionId}_${input.clientRequestId}`;
|
|
42
|
+
thread.turnId = turnId;
|
|
43
|
+
queueMicrotask(() => {
|
|
44
|
+
emit(collectedEvent('notification', 'item/agentMessage/delta', {
|
|
45
|
+
threadId: input.threadId,
|
|
46
|
+
turnId,
|
|
47
|
+
text: `loopback:${input.text}`,
|
|
48
|
+
delta: `loopback:${input.text}`,
|
|
49
|
+
}));
|
|
50
|
+
if (input.text.includes('[approval]')) {
|
|
51
|
+
const approvalId = `approval_${input.clientRequestId}`;
|
|
52
|
+
approvals.add(JSON.stringify([input.sessionId, approvalId]));
|
|
53
|
+
emit(collectedEvent('approval_request', 'item/tool/requestUserInput', {
|
|
54
|
+
threadId: input.threadId,
|
|
55
|
+
turnId,
|
|
56
|
+
approvalId,
|
|
57
|
+
itemId: approvalId,
|
|
58
|
+
}));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
emit(collectedEvent('notification', 'turn/completed', {
|
|
62
|
+
threadId: input.threadId,
|
|
63
|
+
turnId,
|
|
64
|
+
turn: { id: turnId, status: 'completed' },
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
return { outcome: 'accepted', threadId: input.threadId, turnId };
|
|
36
68
|
},
|
|
37
69
|
async interrupt(input) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
const thread = threads.get(input.threadId);
|
|
71
|
+
if (!thread || thread.sessionId !== input.sessionId)
|
|
72
|
+
return { outcome: 'uncertain' };
|
|
73
|
+
const turnId = thread.turnId;
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
emit(collectedEvent('notification', 'turn/completed', {
|
|
76
|
+
threadId: input.threadId,
|
|
77
|
+
turnId,
|
|
78
|
+
turn: { id: turnId, status: 'interrupted' },
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
return { outcome: 'accepted', turnId };
|
|
45
82
|
},
|
|
46
83
|
async respondApproval(input) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
approvals.delete(
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
const key = JSON.stringify([input.sessionId, input.approvalId]);
|
|
85
|
+
if (!approvals.has(key))
|
|
86
|
+
return { outcome: 'failed', approvalId: input.approvalId };
|
|
87
|
+
approvals.delete(key);
|
|
88
|
+
const thread = [...threads].find(([, value]) => value.sessionId === input.sessionId);
|
|
89
|
+
queueMicrotask(() => {
|
|
90
|
+
emit(collectedEvent('notification', 'approval.resolved', {
|
|
91
|
+
approvalId: input.approvalId,
|
|
92
|
+
accepted: input.accepted,
|
|
93
|
+
threadId: thread?.[0],
|
|
94
|
+
turnId: thread?.[1].turnId,
|
|
95
|
+
}));
|
|
96
|
+
emit(collectedEvent('notification', 'item/agentMessage/delta', {
|
|
97
|
+
threadId: thread?.[0],
|
|
98
|
+
turnId: thread?.[1].turnId,
|
|
99
|
+
delta: 'after-approval',
|
|
100
|
+
}));
|
|
101
|
+
emit(collectedEvent('notification', 'turn/completed', {
|
|
102
|
+
threadId: thread?.[0],
|
|
103
|
+
turnId: thread?.[1].turnId,
|
|
104
|
+
turn: { id: thread?.[1].turnId, status: 'completed' },
|
|
105
|
+
}));
|
|
106
|
+
});
|
|
107
|
+
return { outcome: 'accepted', approvalId: input.approvalId, turnId: thread?.[1].turnId };
|
|
57
108
|
},
|
|
58
109
|
};
|
|
59
110
|
}
|