deepline 0.1.19 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +54 -12
- package/dist/cli/index.mjs +54 -12
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/index.mjs +3 -2
- package/dist/repo/apps/play-runner-workers/src/coordinator-entry.ts +149 -68
- package/dist/repo/apps/play-runner-workers/src/dedup-do.ts +85 -60
- package/dist/repo/apps/play-runner-workers/src/entry.ts +144 -15
- package/dist/repo/sdk/src/http.ts +19 -3
- package/dist/repo/sdk/src/version.ts +1 -1
- package/dist/repo/shared_libs/plays/row-identity.ts +59 -4
- package/package.json +1 -1
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
} from '../../../shared_libs/plays/row-identity';
|
|
64
64
|
import {
|
|
65
65
|
getCompiledPipelineSubsteps,
|
|
66
|
+
flattenStaticPipeline,
|
|
66
67
|
resolveSheetContractForTableNamespace,
|
|
67
68
|
sqlSafePlayColumnName,
|
|
68
69
|
type PlayStaticPipeline,
|
|
@@ -365,8 +366,9 @@ function cachedVercelProtectionBypassToken(): string | null {
|
|
|
365
366
|
|
|
366
367
|
const WORKER_PLAY_CALL_LIMITS = {
|
|
367
368
|
maxPlayCallDepth: 6,
|
|
368
|
-
maxPlayCallCount:
|
|
369
|
-
maxChildPlayCallsPerParent:
|
|
369
|
+
maxPlayCallCount: 1_000,
|
|
370
|
+
maxChildPlayCallsPerParent: 1_000,
|
|
371
|
+
maxConcurrentPlayCalls: 16,
|
|
370
372
|
};
|
|
371
373
|
|
|
372
374
|
/**
|
|
@@ -637,6 +639,7 @@ async function postRuntimeApiBestEffort(
|
|
|
637
639
|
async function submitChildPlayThroughCoordinator(input: {
|
|
638
640
|
req: RunRequest;
|
|
639
641
|
body: unknown;
|
|
642
|
+
allowInline?: boolean;
|
|
640
643
|
}): Promise<{
|
|
641
644
|
workflowId?: string;
|
|
642
645
|
runId?: string;
|
|
@@ -648,7 +651,7 @@ async function submitChildPlayThroughCoordinator(input: {
|
|
|
648
651
|
logs?: string[];
|
|
649
652
|
timings?: Array<{ phase: string; ms: number }>;
|
|
650
653
|
}> {
|
|
651
|
-
if (cachedCoordinatorBinding) {
|
|
654
|
+
if (cachedCoordinatorBinding && input.allowInline !== false) {
|
|
652
655
|
if (!isRecord(input.body)) {
|
|
653
656
|
throw new Error('ctx.runPlay child submit requires an object body.');
|
|
654
657
|
}
|
|
@@ -2461,6 +2464,18 @@ function childPipelineUsesCtxMap(
|
|
|
2461
2464
|
);
|
|
2462
2465
|
}
|
|
2463
2466
|
|
|
2467
|
+
function childPipelineNeedsWorkflowScheduler(
|
|
2468
|
+
pipeline: PlayStaticPipeline | null | undefined,
|
|
2469
|
+
): boolean {
|
|
2470
|
+
if (!pipeline) return false;
|
|
2471
|
+
return flattenStaticPipeline(pipeline).some(
|
|
2472
|
+
(substep) =>
|
|
2473
|
+
substep.type === 'tool' &&
|
|
2474
|
+
(substep.isEventWait === true ||
|
|
2475
|
+
substep.toolId === 'test_wait_for_event'),
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2464
2479
|
function releaseChildPlayConcurrency(
|
|
2465
2480
|
inFlightByPlayName: Record<string, number>,
|
|
2466
2481
|
playName: string,
|
|
@@ -2483,6 +2498,41 @@ function createMinimalWorkerCtx(
|
|
|
2483
2498
|
let playCallCount = 0;
|
|
2484
2499
|
const parentChildCalls: Record<string, number> = {};
|
|
2485
2500
|
const inFlightChildCallsByPlayName: Record<string, number> = {};
|
|
2501
|
+
let inFlightChildPlayCalls = 0;
|
|
2502
|
+
const childPlaySlotWaiters: Array<() => void> = [];
|
|
2503
|
+
|
|
2504
|
+
const acquireChildPlaySlot = async (): Promise<() => void> => {
|
|
2505
|
+
while (
|
|
2506
|
+
inFlightChildPlayCalls >= WORKER_PLAY_CALL_LIMITS.maxConcurrentPlayCalls
|
|
2507
|
+
) {
|
|
2508
|
+
await new Promise<void>((resolve, reject) => {
|
|
2509
|
+
const waiter = () => {
|
|
2510
|
+
abortSignal?.removeEventListener('abort', onAbort);
|
|
2511
|
+
resolve();
|
|
2512
|
+
};
|
|
2513
|
+
const onAbort = () => {
|
|
2514
|
+
const index = childPlaySlotWaiters.indexOf(waiter);
|
|
2515
|
+
if (index >= 0) childPlaySlotWaiters.splice(index, 1);
|
|
2516
|
+
reject(
|
|
2517
|
+
abortSignal?.reason instanceof Error
|
|
2518
|
+
? abortSignal.reason
|
|
2519
|
+
: new WorkflowAbortError(),
|
|
2520
|
+
);
|
|
2521
|
+
};
|
|
2522
|
+
childPlaySlotWaiters.push(waiter);
|
|
2523
|
+
abortSignal?.addEventListener('abort', onAbort, { once: true });
|
|
2524
|
+
});
|
|
2525
|
+
assertNotAborted(abortSignal);
|
|
2526
|
+
}
|
|
2527
|
+
inFlightChildPlayCalls += 1;
|
|
2528
|
+
let released = false;
|
|
2529
|
+
return () => {
|
|
2530
|
+
if (released) return;
|
|
2531
|
+
released = true;
|
|
2532
|
+
inFlightChildPlayCalls = Math.max(0, inFlightChildPlayCalls - 1);
|
|
2533
|
+
childPlaySlotWaiters.shift()?.();
|
|
2534
|
+
};
|
|
2535
|
+
};
|
|
2486
2536
|
const rootGovernance = req.playCallGovernance;
|
|
2487
2537
|
const rootRunId = rootGovernance?.rootRunId ?? req.runId;
|
|
2488
2538
|
// Local ancestry chain that always ENDS with the currently-executing play
|
|
@@ -2579,6 +2629,7 @@ function createMinimalWorkerCtx(
|
|
|
2579
2629
|
: JSON.stringify(normalizedParts);
|
|
2580
2630
|
return keyValue;
|
|
2581
2631
|
};
|
|
2632
|
+
const mapLogicFingerprint = req.graphHash ?? null;
|
|
2582
2633
|
const resolveRowKey = (
|
|
2583
2634
|
row: Record<string, unknown>,
|
|
2584
2635
|
index: number,
|
|
@@ -2586,8 +2637,12 @@ function createMinimalWorkerCtx(
|
|
|
2586
2637
|
const inputRow = publicCsvInputRow(row);
|
|
2587
2638
|
const explicitKeyValue = resolveExplicitKeyValue(row, index);
|
|
2588
2639
|
return explicitKeyValue == null
|
|
2589
|
-
? derivePlayRowIdentity(inputRow, name)
|
|
2590
|
-
: derivePlayRowIdentityFromKey(
|
|
2640
|
+
? derivePlayRowIdentity(inputRow, name, mapLogicFingerprint)
|
|
2641
|
+
: derivePlayRowIdentityFromKey(
|
|
2642
|
+
explicitKeyValue,
|
|
2643
|
+
name,
|
|
2644
|
+
mapLogicFingerprint,
|
|
2645
|
+
);
|
|
2591
2646
|
};
|
|
2592
2647
|
const assertUniqueExplicitRowKeys = (
|
|
2593
2648
|
chunkRows: readonly Record<string, unknown>[],
|
|
@@ -2635,7 +2690,11 @@ function createMinimalWorkerCtx(
|
|
|
2635
2690
|
const key =
|
|
2636
2691
|
typeof row.__deeplineRowKey === 'string'
|
|
2637
2692
|
? row.__deeplineRowKey
|
|
2638
|
-
: derivePlayRowIdentity(
|
|
2693
|
+
: derivePlayRowIdentity(
|
|
2694
|
+
publicCsvInputRow(row),
|
|
2695
|
+
name,
|
|
2696
|
+
mapLogicFingerprint,
|
|
2697
|
+
);
|
|
2639
2698
|
if (key) {
|
|
2640
2699
|
pendingKeys.add(key);
|
|
2641
2700
|
preparedKeys.add(key);
|
|
@@ -2645,7 +2704,11 @@ function createMinimalWorkerCtx(
|
|
|
2645
2704
|
const key =
|
|
2646
2705
|
typeof row.__deeplineRowKey === 'string'
|
|
2647
2706
|
? row.__deeplineRowKey
|
|
2648
|
-
: derivePlayRowIdentity(
|
|
2707
|
+
: derivePlayRowIdentity(
|
|
2708
|
+
publicCsvInputRow(row),
|
|
2709
|
+
name,
|
|
2710
|
+
mapLogicFingerprint,
|
|
2711
|
+
);
|
|
2649
2712
|
if (key) {
|
|
2650
2713
|
completedKeys.add(key);
|
|
2651
2714
|
preparedKeys.add(key);
|
|
@@ -2852,7 +2915,11 @@ function createMinimalWorkerCtx(
|
|
|
2852
2915
|
const key =
|
|
2853
2916
|
typeof completedRow.__deeplineRowKey === 'string'
|
|
2854
2917
|
? completedRow.__deeplineRowKey
|
|
2855
|
-
: derivePlayRowIdentity(
|
|
2918
|
+
: derivePlayRowIdentity(
|
|
2919
|
+
publicCsvInputRow(completedRow),
|
|
2920
|
+
name,
|
|
2921
|
+
mapLogicFingerprint,
|
|
2922
|
+
);
|
|
2856
2923
|
if (key) {
|
|
2857
2924
|
const { __deeplineRowKey: _rowKey, ...cleanedRow } =
|
|
2858
2925
|
publicCsvInputRow(completedRow);
|
|
@@ -3227,7 +3294,11 @@ function createMinimalWorkerCtx(
|
|
|
3227
3294
|
const completedKeys = new Set<string>();
|
|
3228
3295
|
const preparedKeys = new Set<string>();
|
|
3229
3296
|
for (const row of prepared.pendingRows) {
|
|
3230
|
-
const key = derivePlayRowIdentity(
|
|
3297
|
+
const key = derivePlayRowIdentity(
|
|
3298
|
+
publicCsvInputRow(row),
|
|
3299
|
+
name,
|
|
3300
|
+
mapLogicFingerprint,
|
|
3301
|
+
);
|
|
3231
3302
|
if (key) {
|
|
3232
3303
|
pendingKeys.add(key);
|
|
3233
3304
|
preparedKeys.add(key);
|
|
@@ -3237,18 +3308,30 @@ function createMinimalWorkerCtx(
|
|
|
3237
3308
|
const key =
|
|
3238
3309
|
typeof row.__deeplineRowKey === 'string'
|
|
3239
3310
|
? row.__deeplineRowKey
|
|
3240
|
-
: derivePlayRowIdentity(
|
|
3311
|
+
: derivePlayRowIdentity(
|
|
3312
|
+
publicCsvInputRow(row),
|
|
3313
|
+
name,
|
|
3314
|
+
mapLogicFingerprint,
|
|
3315
|
+
);
|
|
3241
3316
|
if (key) {
|
|
3242
3317
|
completedKeys.add(key);
|
|
3243
3318
|
preparedKeys.add(key);
|
|
3244
3319
|
}
|
|
3245
3320
|
}
|
|
3246
3321
|
const missingPreparedRows = chunkRows.filter((row) => {
|
|
3247
|
-
const key = derivePlayRowIdentity(
|
|
3322
|
+
const key = derivePlayRowIdentity(
|
|
3323
|
+
publicCsvInputRow(row),
|
|
3324
|
+
name,
|
|
3325
|
+
mapLogicFingerprint,
|
|
3326
|
+
);
|
|
3248
3327
|
return !key || !preparedKeys.has(key);
|
|
3249
3328
|
});
|
|
3250
3329
|
const rowsToExecute = chunkRows.filter((row) => {
|
|
3251
|
-
const key = derivePlayRowIdentity(
|
|
3330
|
+
const key = derivePlayRowIdentity(
|
|
3331
|
+
publicCsvInputRow(row),
|
|
3332
|
+
name,
|
|
3333
|
+
mapLogicFingerprint,
|
|
3334
|
+
);
|
|
3252
3335
|
return !key || pendingKeys.has(key) || !completedKeys.has(key);
|
|
3253
3336
|
});
|
|
3254
3337
|
const rowsInserted = prepared.inserted + missingPreparedRows.length;
|
|
@@ -3331,6 +3414,7 @@ function createMinimalWorkerCtx(
|
|
|
3331
3414
|
__deeplineRowKey: derivePlayRowIdentity(
|
|
3332
3415
|
publicCsvInputRow(rowsToExecute[executedIndex]!),
|
|
3333
3416
|
name,
|
|
3417
|
+
mapLogicFingerprint,
|
|
3334
3418
|
),
|
|
3335
3419
|
})),
|
|
3336
3420
|
});
|
|
@@ -3340,7 +3424,11 @@ function createMinimalWorkerCtx(
|
|
|
3340
3424
|
const key =
|
|
3341
3425
|
typeof completedRow.__deeplineRowKey === 'string'
|
|
3342
3426
|
? completedRow.__deeplineRowKey
|
|
3343
|
-
: derivePlayRowIdentity(
|
|
3427
|
+
: derivePlayRowIdentity(
|
|
3428
|
+
publicCsvInputRow(completedRow),
|
|
3429
|
+
name,
|
|
3430
|
+
mapLogicFingerprint,
|
|
3431
|
+
);
|
|
3344
3432
|
if (key) {
|
|
3345
3433
|
const { __deeplineRowKey: _rowKey, ...cleanedRow } =
|
|
3346
3434
|
publicCsvInputRow(completedRow);
|
|
@@ -3357,12 +3445,17 @@ function createMinimalWorkerCtx(
|
|
|
3357
3445
|
const key = derivePlayRowIdentity(
|
|
3358
3446
|
publicCsvInputRow(rowsToExecute[executedIndex]!),
|
|
3359
3447
|
name,
|
|
3448
|
+
mapLogicFingerprint,
|
|
3360
3449
|
);
|
|
3361
3450
|
if (key) resultByKey.set(key, executedRow);
|
|
3362
3451
|
}
|
|
3363
3452
|
const out = chunkRows
|
|
3364
3453
|
.map((row) => {
|
|
3365
|
-
const key = derivePlayRowIdentity(
|
|
3454
|
+
const key = derivePlayRowIdentity(
|
|
3455
|
+
publicCsvInputRow(row),
|
|
3456
|
+
name,
|
|
3457
|
+
mapLogicFingerprint,
|
|
3458
|
+
);
|
|
3366
3459
|
return key ? resultByKey.get(key) : undefined;
|
|
3367
3460
|
})
|
|
3368
3461
|
.filter((row): row is T & Record<string, unknown> => Boolean(row));
|
|
@@ -3620,7 +3713,11 @@ function createMinimalWorkerCtx(
|
|
|
3620
3713
|
const childIsMapBacked = childPipelineUsesCtxMap(
|
|
3621
3714
|
childManifest.staticPipeline,
|
|
3622
3715
|
);
|
|
3716
|
+
const childNeedsWorkflowScheduler = childPipelineNeedsWorkflowScheduler(
|
|
3717
|
+
childManifest.staticPipeline,
|
|
3718
|
+
);
|
|
3623
3719
|
let childConcurrencyAcquired = false;
|
|
3720
|
+
let releaseChildPlaySlot: (() => void) | null = null;
|
|
3624
3721
|
if (childIsMapBacked) {
|
|
3625
3722
|
const nextInFlight =
|
|
3626
3723
|
(inFlightChildCallsByPlayName[resolvedName] ?? 0) + 1;
|
|
@@ -3635,11 +3732,21 @@ function createMinimalWorkerCtx(
|
|
|
3635
3732
|
childConcurrencyAcquired = true;
|
|
3636
3733
|
}
|
|
3637
3734
|
try {
|
|
3735
|
+
releaseChildPlaySlot = await acquireChildPlaySlot();
|
|
3638
3736
|
const childSubmitStartedAt = nowMs();
|
|
3639
|
-
let started: {
|
|
3737
|
+
let started: {
|
|
3738
|
+
workflowId?: string;
|
|
3739
|
+
runId?: string;
|
|
3740
|
+
status?: string;
|
|
3741
|
+
output?: unknown;
|
|
3742
|
+
result?: unknown;
|
|
3743
|
+
error?: unknown;
|
|
3744
|
+
};
|
|
3640
3745
|
try {
|
|
3641
3746
|
started = await submitChildPlayThroughCoordinator({
|
|
3642
3747
|
req,
|
|
3748
|
+
allowInline:
|
|
3749
|
+
options?.timeoutMs == null && !childNeedsWorkflowScheduler,
|
|
3643
3750
|
body: {
|
|
3644
3751
|
name: resolvedName,
|
|
3645
3752
|
input: isRecord(input) ? input : {},
|
|
@@ -3709,6 +3816,27 @@ function createMinimalWorkerCtx(
|
|
|
3709
3816
|
ms: nowMs() - childSubmitStartedAt,
|
|
3710
3817
|
status: 'ok',
|
|
3711
3818
|
});
|
|
3819
|
+
const startedStatus = String(started.status ?? '').toLowerCase();
|
|
3820
|
+
if (startedStatus === 'completed') {
|
|
3821
|
+
emitEvent({
|
|
3822
|
+
type: 'log',
|
|
3823
|
+
level: 'info',
|
|
3824
|
+
message: `Completed child play ${resolvedName} (${normalizedKey})`,
|
|
3825
|
+
ts: nowMs(),
|
|
3826
|
+
});
|
|
3827
|
+
return started.output ?? extractChildPlayOutput(started);
|
|
3828
|
+
}
|
|
3829
|
+
if (startedStatus === 'failed') {
|
|
3830
|
+
const startedError = isRecord(started.error)
|
|
3831
|
+
? started.error
|
|
3832
|
+
: { message: started.error };
|
|
3833
|
+
const startedErrorMessage =
|
|
3834
|
+
typeof startedError.message === 'string' &&
|
|
3835
|
+
startedError.message.trim()
|
|
3836
|
+
? startedError.message.trim()
|
|
3837
|
+
: `Child play ${resolvedName} (${workflowId}) failed.`;
|
|
3838
|
+
throw new Error(startedErrorMessage);
|
|
3839
|
+
}
|
|
3712
3840
|
const childWaitStartedAt = nowMs();
|
|
3713
3841
|
let result: unknown;
|
|
3714
3842
|
try {
|
|
@@ -3761,6 +3889,7 @@ function createMinimalWorkerCtx(
|
|
|
3761
3889
|
});
|
|
3762
3890
|
return result;
|
|
3763
3891
|
} finally {
|
|
3892
|
+
releaseChildPlaySlot?.();
|
|
3764
3893
|
if (childConcurrencyAcquired) {
|
|
3765
3894
|
releaseChildPlayConcurrency(
|
|
3766
3895
|
inFlightChildCallsByPlayName,
|
|
@@ -179,10 +179,26 @@ export class HttpClient {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
if (!response.ok) {
|
|
182
|
-
const
|
|
182
|
+
const errorValue =
|
|
183
183
|
typeof parsed === 'object' && parsed && 'error' in parsed
|
|
184
|
-
?
|
|
185
|
-
:
|
|
184
|
+
? (parsed as Record<string, unknown>).error
|
|
185
|
+
: undefined;
|
|
186
|
+
const msg =
|
|
187
|
+
typeof errorValue === 'string'
|
|
188
|
+
? errorValue
|
|
189
|
+
: errorValue &&
|
|
190
|
+
typeof errorValue === 'object' &&
|
|
191
|
+
'message' in errorValue &&
|
|
192
|
+
typeof (errorValue as Record<string, unknown>).message ===
|
|
193
|
+
'string'
|
|
194
|
+
? (errorValue as Record<string, string>).message
|
|
195
|
+
: typeof parsed === 'object' &&
|
|
196
|
+
parsed &&
|
|
197
|
+
'message' in parsed &&
|
|
198
|
+
typeof (parsed as Record<string, unknown>).message ===
|
|
199
|
+
'string'
|
|
200
|
+
? (parsed as Record<string, string>).message
|
|
201
|
+
: `HTTP ${response.status}`;
|
|
186
202
|
throw new DeeplineError(msg, response.status, 'API_ERROR', {
|
|
187
203
|
response: parsed,
|
|
188
204
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.1.
|
|
1
|
+
export const SDK_VERSION = "0.1.20";
|
|
2
2
|
export const SDK_API_CONTRACT = "2026-05-runs-v2";
|
|
@@ -276,13 +276,64 @@ export function resolvePlayRunTableNamespace(
|
|
|
276
276
|
export function derivePlayRowIdentity(
|
|
277
277
|
row: Record<string, unknown>,
|
|
278
278
|
tableNamespace: string,
|
|
279
|
+
logicFingerprint?: string | null,
|
|
279
280
|
): string {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
return deriveDerivedOutputIdentity({
|
|
282
|
+
inputItem: row,
|
|
283
|
+
operationNamespace: tableNamespace,
|
|
284
|
+
logicFingerprint,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function deriveDerivedOutputIdentity(input: {
|
|
289
|
+
inputItem: Record<string, unknown>;
|
|
290
|
+
operationNamespace: string;
|
|
291
|
+
logicFingerprint?: string | null;
|
|
292
|
+
}): string {
|
|
293
|
+
const normalizedNamespace = normalizeTableNamespace(
|
|
294
|
+
input.operationNamespace,
|
|
295
|
+
);
|
|
296
|
+
const canonicalRow = stableStringify(input.inputItem);
|
|
297
|
+
const fingerprint = input.logicFingerprint?.trim()
|
|
298
|
+
? `\nlogic:${input.logicFingerprint.trim()}`
|
|
299
|
+
: '';
|
|
300
|
+
const digest = sha256Hex(
|
|
301
|
+
`${normalizedNamespace}${fingerprint}\n${canonicalRow}`,
|
|
302
|
+
);
|
|
283
303
|
return `${normalizedNamespace}:${digest}`;
|
|
284
304
|
}
|
|
285
305
|
|
|
306
|
+
export function deriveToolRequestIdentity(input: {
|
|
307
|
+
toolId: string;
|
|
308
|
+
requestInput: Record<string, unknown>;
|
|
309
|
+
effectiveAccountContext?: string | null;
|
|
310
|
+
toolContractRevision?: string | number | null;
|
|
311
|
+
reuseSafetyPolicy?: string | null;
|
|
312
|
+
}): string {
|
|
313
|
+
const toolId = input.toolId.trim();
|
|
314
|
+
if (!toolId) {
|
|
315
|
+
throw new Error('Tool request identity requires a non-empty tool id.');
|
|
316
|
+
}
|
|
317
|
+
const accountContext =
|
|
318
|
+
input.effectiveAccountContext?.trim() || 'default_account_context';
|
|
319
|
+
const contractRevision =
|
|
320
|
+
input.toolContractRevision == null
|
|
321
|
+
? 'default_tool_contract'
|
|
322
|
+
: String(input.toolContractRevision).trim() || 'default_tool_contract';
|
|
323
|
+
const reuseSafetyPolicy =
|
|
324
|
+
input.reuseSafetyPolicy?.trim() || 'default_reuse_policy';
|
|
325
|
+
const digest = sha256Hex(
|
|
326
|
+
stableStringify({
|
|
327
|
+
accountContext,
|
|
328
|
+
requestInput: input.requestInput,
|
|
329
|
+
reuseSafetyPolicy,
|
|
330
|
+
toolContractRevision: contractRevision,
|
|
331
|
+
toolId,
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
return `tool:${normalizeTableNamespace(toolId)}:${digest}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
286
337
|
/**
|
|
287
338
|
* Build a stable row identity from an explicit user-provided key string.
|
|
288
339
|
*
|
|
@@ -293,9 +344,13 @@ export function derivePlayRowIdentity(
|
|
|
293
344
|
export function derivePlayRowIdentityFromKey(
|
|
294
345
|
key: string,
|
|
295
346
|
tableNamespace: string,
|
|
347
|
+
logicFingerprint?: string | null,
|
|
296
348
|
): string {
|
|
297
349
|
const normalizedNamespace = normalizeTableNamespace(tableNamespace);
|
|
298
|
-
const
|
|
350
|
+
const fingerprint = logicFingerprint?.trim()
|
|
351
|
+
? `\nlogic:${logicFingerprint.trim()}`
|
|
352
|
+
: '';
|
|
353
|
+
const digest = sha256Hex(`${normalizedNamespace}${fingerprint}\nkey:${key}`);
|
|
299
354
|
return `${normalizedNamespace}:${digest}`;
|
|
300
355
|
}
|
|
301
356
|
|