iranti 0.3.30 → 0.3.32
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/README.md +43 -0
- package/dist/src/api/server.js +208 -32
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/attendant/AttendantInstance.d.ts +17 -0
- package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
- package/dist/src/attendant/AttendantInstance.js +279 -0
- package/dist/src/attendant/AttendantInstance.js.map +1 -1
- package/dist/src/staff/subTurnLoop.d.ts +47 -0
- package/dist/src/staff/subTurnLoop.d.ts.map +1 -0
- package/dist/src/staff/subTurnLoop.js +259 -0
- package/dist/src/staff/subTurnLoop.js.map +1 -0
- package/package.json +3 -1
|
@@ -7,6 +7,7 @@ exports.extractRuleTriggers = extractRuleTriggers;
|
|
|
7
7
|
exports.matchesRuleTriggers = matchesRuleTriggers;
|
|
8
8
|
exports.formatMatchedUserRules = formatMatchedUserRules;
|
|
9
9
|
exports.extractFilePathEntityHints = extractFilePathEntityHints;
|
|
10
|
+
exports.detectFileAction = detectFileAction;
|
|
10
11
|
exports.resolveAutowriteTargetEntity = resolveAutowriteTargetEntity;
|
|
11
12
|
exports.parseAutowriteEntity = parseAutowriteEntity;
|
|
12
13
|
exports.buildToolResultContextLabel = buildToolResultContextLabel;
|
|
@@ -40,6 +41,7 @@ const sessionLedger_1 = require("../lib/sessionLedger");
|
|
|
40
41
|
const sharedStateInvalidation_1 = require("../lib/sharedStateInvalidation");
|
|
41
42
|
const hostMemoryFormatting_1 = require("../lib/hostMemoryFormatting");
|
|
42
43
|
const council_1 = require("../staff/council");
|
|
44
|
+
const subTurnLoop_1 = require("../staff/subTurnLoop");
|
|
43
45
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
44
46
|
const ATTENDANT_RULES_QUERY = {
|
|
45
47
|
entityType: 'system',
|
|
@@ -96,6 +98,10 @@ const TOOL_COST_WRITE_NUDGE_THRESHOLD = 5;
|
|
|
96
98
|
// concrete draft facts instead of a vague "write something" reminder.
|
|
97
99
|
const RECENT_AUTOWRITE_BUFFER_LIMIT = 12;
|
|
98
100
|
const AUTOWRITE_SOURCE_TAG = 'attendant_autowrite';
|
|
101
|
+
// M7: response file-change autowrite — constants governing the scan pass
|
|
102
|
+
// that runs at post-response to extract file paths from the agent's reply.
|
|
103
|
+
const RESPONSE_FILE_CAPTURE_MIN_CHARS = 50;
|
|
104
|
+
const RESPONSE_FILE_CAPTURE_MAX_FILES = 20;
|
|
99
105
|
const MIN_ENTITY_CONFIDENCE = 0.75;
|
|
100
106
|
const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
|
|
101
107
|
const LEDGER_WORKING_MEMORY_PREFIX = 'system/session_ledger/recent_learning_';
|
|
@@ -527,6 +533,18 @@ function scanStringForPathEntities(text, projectId, seen, hints) {
|
|
|
527
533
|
deriveFileEntityFromPath(match[1], projectId, seen, hints);
|
|
528
534
|
}
|
|
529
535
|
}
|
|
536
|
+
// M7: infer whether a file path mention in the agent's response represents
|
|
537
|
+
// an edit, a creation, or a read. Scans the ±150-char context window for
|
|
538
|
+
// action-bearing keywords. Defaults to 'read' when the signal is ambiguous.
|
|
539
|
+
// Exported for direct unit testing.
|
|
540
|
+
function detectFileAction(contextWindow) {
|
|
541
|
+
const ctx = contextWindow.toLowerCase();
|
|
542
|
+
if (/\bcreat\w*\b|\bnew file\b|\badded new\b|\bwrote new\b/.test(ctx))
|
|
543
|
+
return 'created';
|
|
544
|
+
if (/\bedit\w*\b|\bupdat\w*\b|\bmodif\w*\b|\bchanged\b|\bwrote\b|\brewrote\b|\brefactor\w*\b/.test(ctx))
|
|
545
|
+
return 'edited';
|
|
546
|
+
return 'read';
|
|
547
|
+
}
|
|
530
548
|
function extractBasenameFromGlobPattern(pattern) {
|
|
531
549
|
// A glob like "src/**/*.ts" has no meaningful basename; one like
|
|
532
550
|
// "tests/attendant/run_mid_turn_attend_tests.ts" does. We only derive a
|
|
@@ -3280,6 +3298,149 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
3280
3298
|
}
|
|
3281
3299
|
return baseOutcome;
|
|
3282
3300
|
}
|
|
3301
|
+
// M7: scan the agent's post-response text for file path mentions, infer
|
|
3302
|
+
// the most likely action (edited / created / read) from surrounding context,
|
|
3303
|
+
// and write one durable fact per file-scoped entity. This populates the
|
|
3304
|
+
// project/{id}/file/{basename} entity namespace so demand-driven recall
|
|
3305
|
+
// (extractFilePathEntityHints + observe) can surface the facts in future
|
|
3306
|
+
// sessions without any host-side collection effort.
|
|
3307
|
+
//
|
|
3308
|
+
// Isolation policy: extraction failures (regex errors, downstream write
|
|
3309
|
+
// errors) MUST NOT abort the attend call. This method swallows its own
|
|
3310
|
+
// errors and reports them via the skipped array.
|
|
3311
|
+
async runResponseFileCapture(responseText, batchId) {
|
|
3312
|
+
const t0 = Date.now();
|
|
3313
|
+
const skipped = [];
|
|
3314
|
+
const baseResult = {
|
|
3315
|
+
autowriteBatchId: batchId,
|
|
3316
|
+
filesDetected: 0,
|
|
3317
|
+
factsWritten: 0,
|
|
3318
|
+
entities: [],
|
|
3319
|
+
skipped,
|
|
3320
|
+
durationMs: 0,
|
|
3321
|
+
};
|
|
3322
|
+
if (!responseText || responseText.trim().length < RESPONSE_FILE_CAPTURE_MIN_CHARS) {
|
|
3323
|
+
skipped.push({ reason: 'response_too_short' });
|
|
3324
|
+
baseResult.durationMs = Date.now() - t0;
|
|
3325
|
+
return baseResult;
|
|
3326
|
+
}
|
|
3327
|
+
const projectEntity = (0, autoRemember_1.getProjectMemoryEntity)() ?? null;
|
|
3328
|
+
if (!projectEntity) {
|
|
3329
|
+
skipped.push({ reason: 'no_project_entity' });
|
|
3330
|
+
baseResult.durationMs = Date.now() - t0;
|
|
3331
|
+
return baseResult;
|
|
3332
|
+
}
|
|
3333
|
+
const projectId = (0, entity_resolution_1.parseEntityString)(projectEntity).entityId;
|
|
3334
|
+
// Scan the response text for file path occurrences. One fact per
|
|
3335
|
+
// distinct basename — first occurrence wins if the same file appears
|
|
3336
|
+
// multiple times in the same response.
|
|
3337
|
+
const seenEntities = new Set();
|
|
3338
|
+
const fileOccurrences = [];
|
|
3339
|
+
FILE_PATH_PATTERN.lastIndex = 0;
|
|
3340
|
+
let match;
|
|
3341
|
+
while ((match = FILE_PATH_PATTERN.exec(responseText)) !== null) {
|
|
3342
|
+
const filePath = match[1].trim();
|
|
3343
|
+
const basename = filePath.replace(/\\/g, '/').split('/').pop() ?? '';
|
|
3344
|
+
const stripped = basename.split(/[,:()]/)[0] ?? basename;
|
|
3345
|
+
const nameWithoutExt = stripped
|
|
3346
|
+
.replace(/\.\w+$/, '')
|
|
3347
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
3348
|
+
.toLowerCase();
|
|
3349
|
+
if (!nameWithoutExt || nameWithoutExt.length < 2)
|
|
3350
|
+
continue;
|
|
3351
|
+
const entity = `project/${projectId}/file/${nameWithoutExt}`;
|
|
3352
|
+
if (seenEntities.has(entity))
|
|
3353
|
+
continue;
|
|
3354
|
+
seenEntities.add(entity);
|
|
3355
|
+
// Grab a context window around the match for action detection.
|
|
3356
|
+
const matchStart = match.index;
|
|
3357
|
+
const contextStart = Math.max(0, matchStart - 150);
|
|
3358
|
+
const contextEnd = Math.min(responseText.length, matchStart + filePath.length + 150);
|
|
3359
|
+
const contextWindow = responseText.slice(contextStart, contextEnd);
|
|
3360
|
+
fileOccurrences.push({ path: filePath, entity, contextWindow });
|
|
3361
|
+
if (fileOccurrences.length >= RESPONSE_FILE_CAPTURE_MAX_FILES)
|
|
3362
|
+
break;
|
|
3363
|
+
}
|
|
3364
|
+
baseResult.filesDetected = fileOccurrences.length;
|
|
3365
|
+
if (fileOccurrences.length === 0) {
|
|
3366
|
+
baseResult.durationMs = Date.now() - t0;
|
|
3367
|
+
return baseResult;
|
|
3368
|
+
}
|
|
3369
|
+
const writtenEntities = [];
|
|
3370
|
+
for (const { path, entity, contextWindow } of fileOccurrences) {
|
|
3371
|
+
const action = detectFileAction(contextWindow);
|
|
3372
|
+
const key = (action === 'edited' || action === 'created') ? 'last_edit' : 'last_access';
|
|
3373
|
+
const confidence = (action === 'edited' || action === 'created') ? 85 : 70;
|
|
3374
|
+
const parsedEntity = parseAutowriteEntity(entity);
|
|
3375
|
+
const displayName = path.replace(/\\/g, '/').split('/').pop() ?? path;
|
|
3376
|
+
const value = {
|
|
3377
|
+
action,
|
|
3378
|
+
path,
|
|
3379
|
+
capturedAt: new Date().toISOString(),
|
|
3380
|
+
};
|
|
3381
|
+
const summary = `${displayName} was ${action} — captured from agent response at post-response.`;
|
|
3382
|
+
try {
|
|
3383
|
+
await (0, librarian_1.librarianWrite)({
|
|
3384
|
+
entityType: parsedEntity.entityType,
|
|
3385
|
+
entityId: parsedEntity.entityId,
|
|
3386
|
+
key,
|
|
3387
|
+
valueRaw: value,
|
|
3388
|
+
valueSummary: summary.slice(0, 500),
|
|
3389
|
+
confidence,
|
|
3390
|
+
source: AUTOWRITE_SOURCE_TAG,
|
|
3391
|
+
createdBy: `attendant:${this.agentId}`,
|
|
3392
|
+
properties: {
|
|
3393
|
+
memoryScope: 'project',
|
|
3394
|
+
capturePhase: 'response_file_change_autowrite',
|
|
3395
|
+
autowriteBatchId: batchId,
|
|
3396
|
+
fileAction: action,
|
|
3397
|
+
autowriteOccurredAt: new Date().toISOString(),
|
|
3398
|
+
...(0, semanticFactTags_1.buildSemanticFactTags)({
|
|
3399
|
+
memoryScope: 'project',
|
|
3400
|
+
durableClass: 'file_change',
|
|
3401
|
+
mergeStrategy: 'replace',
|
|
3402
|
+
extraTags: ['autowrite', 'response_capture', `action:${action}`],
|
|
3403
|
+
}),
|
|
3404
|
+
},
|
|
3405
|
+
});
|
|
3406
|
+
writtenEntities.push(entity);
|
|
3407
|
+
}
|
|
3408
|
+
catch (err) {
|
|
3409
|
+
skipped.push({
|
|
3410
|
+
reason: 'librarian_write_failed',
|
|
3411
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
3412
|
+
});
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
baseResult.factsWritten = writtenEntities.length;
|
|
3416
|
+
baseResult.entities = writtenEntities;
|
|
3417
|
+
baseResult.durationMs = Date.now() - t0;
|
|
3418
|
+
if (writtenEntities.length > 0) {
|
|
3419
|
+
// Autowriting file-change facts counts as durable persistence —
|
|
3420
|
+
// reset pressure counters and notify compliance tracking.
|
|
3421
|
+
this.attendsWithoutPersist = 0;
|
|
3422
|
+
this.turnsWithoutWrite = 0;
|
|
3423
|
+
this.writeOccurredThisTurn = true;
|
|
3424
|
+
this.recordMemoryEvidence('write');
|
|
3425
|
+
if (this.eventSource) {
|
|
3426
|
+
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|
|
3427
|
+
staffComponent: 'Attendant',
|
|
3428
|
+
actionType: 'attendant_autowrite',
|
|
3429
|
+
agentId: this.agentId,
|
|
3430
|
+
source: this.eventSource,
|
|
3431
|
+
reason: 'response_file_change_autowrite',
|
|
3432
|
+
level: 'audit',
|
|
3433
|
+
metadata: this.buildEventMetadata({
|
|
3434
|
+
autowriteBatchId: batchId,
|
|
3435
|
+
filesDetected: baseResult.filesDetected,
|
|
3436
|
+
factsWritten: baseResult.factsWritten,
|
|
3437
|
+
entities: writtenEntities,
|
|
3438
|
+
}),
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return baseResult;
|
|
3443
|
+
}
|
|
3283
3444
|
// M3: build a forced write-nudge payload when tool-cost pressure crosses
|
|
3284
3445
|
// TOOL_COST_WRITE_NUDGE_THRESHOLD and the nudge has not already been
|
|
3285
3446
|
// emitted this turn. Returns null when no nudge should fire. The nudge
|
|
@@ -3741,6 +3902,10 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
3741
3902
|
}
|
|
3742
3903
|
if (phase === 'post-response') {
|
|
3743
3904
|
const memoryAttributions = this.scorePendingMemoryAttributions(latestMessage || currentContext);
|
|
3905
|
+
// M7: scan the agent's response for file path mentions and write file-scoped facts.
|
|
3906
|
+
// Must run BEFORE buildComplianceState so that any writes here reset the
|
|
3907
|
+
// pressure counters and are reflected in the compliance snapshot.
|
|
3908
|
+
const responseFileCapture = await this.runResponseFileCapture(latestMessage, `rfca_${Date.now()}`);
|
|
3744
3909
|
compliance = this.buildComplianceState(this.complianceUpdatedAt);
|
|
3745
3910
|
if (memoryAttributions.some((entry) => !entry.used)) {
|
|
3746
3911
|
complianceWarning = ignoredMemoryWarning;
|
|
@@ -3827,6 +3992,23 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
3827
3992
|
outcome: 'no_context',
|
|
3828
3993
|
budgetMax: 0,
|
|
3829
3994
|
},
|
|
3995
|
+
// B8 M6: sub-turn reasoning loop is gated off on post-response.
|
|
3996
|
+
// Field echoed as a declined shell for destructure parity.
|
|
3997
|
+
subTurnLoopPlan: {
|
|
3998
|
+
outcome: 'declined_post_response',
|
|
3999
|
+
attempted: false,
|
|
4000
|
+
partialResponseLength: 0,
|
|
4001
|
+
initialFactCount: 0,
|
|
4002
|
+
addedFactCount: 0,
|
|
4003
|
+
rescoreTokens: [],
|
|
4004
|
+
proposedHints: [],
|
|
4005
|
+
budgetMax: 0,
|
|
4006
|
+
reason: 'post_response_closeout',
|
|
4007
|
+
note: 'Sub-turn loop is gated off on post-response attends.',
|
|
4008
|
+
},
|
|
4009
|
+
// M7: response file-change autowrite — populated at post-response.
|
|
4010
|
+
// Contains the outcome of the scan pass over the agent's reply.
|
|
4011
|
+
responseFileCapture,
|
|
3830
4012
|
};
|
|
3831
4013
|
}
|
|
3832
4014
|
let decision = await this.decideMemoryNeed({
|
|
@@ -4005,6 +4187,17 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
4005
4187
|
lowConfidenceCount: 0,
|
|
4006
4188
|
},
|
|
4007
4189
|
}),
|
|
4190
|
+
// B8 M6: sub-turn loop plan populated even on the not-needed
|
|
4191
|
+
// path — the helper will decline cleanly because no retrieval
|
|
4192
|
+
// ran (initialFactCount=0) and, on non-mid-turn phases, the
|
|
4193
|
+
// phase gate fires first. Pure helper so no latency concern.
|
|
4194
|
+
subTurnLoopPlan: (0, subTurnLoop_1.planSubTurnLoop)({
|
|
4195
|
+
phase,
|
|
4196
|
+
partialResponse: input.partialResponse,
|
|
4197
|
+
latestMessage,
|
|
4198
|
+
originalEntityHints: effectiveEntityHints,
|
|
4199
|
+
initialFactCount: 0,
|
|
4200
|
+
}),
|
|
4008
4201
|
};
|
|
4009
4202
|
}
|
|
4010
4203
|
// Post-compaction recovery: re-surface facts that were recently injected (likely in context
|
|
@@ -4137,6 +4330,85 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
4137
4330
|
note: 'No refinement pass required.',
|
|
4138
4331
|
};
|
|
4139
4332
|
}
|
|
4333
|
+
// B8 M6: sub-turn reasoning loop. When the caller passed a partial
|
|
4334
|
+
// assistant response on a mid-turn attend, re-score that partial
|
|
4335
|
+
// against the baseline retrieval and decide whether to fire ONE
|
|
4336
|
+
// bounded extra observe() call with broadened hints harvested from
|
|
4337
|
+
// the partial text. This is A3's refinement pattern re-applied on
|
|
4338
|
+
// response progress rather than initial-pass emptiness. Pure plan
|
|
4339
|
+
// + at most one retry; same union-of-original+proposed hint fix as
|
|
4340
|
+
// B6's refinement retry above. Gates ensure it only fires on
|
|
4341
|
+
// mid-turn with a non-trivial partial response carrying novel
|
|
4342
|
+
// tokens or type/id patterns beyond the baseline.
|
|
4343
|
+
const subTurnInitialCount = augmentedObservedFacts.length;
|
|
4344
|
+
let subTurnLoopPlan = (0, subTurnLoop_1.planSubTurnLoop)({
|
|
4345
|
+
phase,
|
|
4346
|
+
partialResponse: input.partialResponse,
|
|
4347
|
+
latestMessage,
|
|
4348
|
+
originalEntityHints: allObserveEntityHints,
|
|
4349
|
+
initialFactCount: subTurnInitialCount,
|
|
4350
|
+
});
|
|
4351
|
+
if (subTurnLoopPlan.attempted && subTurnLoopPlan.proposedHints.length > 0) {
|
|
4352
|
+
// Retry uses the UNION of allObserveEntityHints and the
|
|
4353
|
+
// sub-turn proposed hints — same preservation fix as B6 so a
|
|
4354
|
+
// specific host-supplied hint is not dropped when the sub-turn
|
|
4355
|
+
// widens with patterns harvested from the partial response.
|
|
4356
|
+
const subTurnRetryHints = Array.from(new Set([
|
|
4357
|
+
...allObserveEntityHints,
|
|
4358
|
+
...subTurnLoopPlan.proposedHints,
|
|
4359
|
+
]));
|
|
4360
|
+
try {
|
|
4361
|
+
const subTurnObserved = await this.observe({
|
|
4362
|
+
currentContext: observationContext,
|
|
4363
|
+
maxFacts: effectiveMaxFacts,
|
|
4364
|
+
entityHints: subTurnRetryHints,
|
|
4365
|
+
priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
|
|
4366
|
+
...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
|
|
4367
|
+
...(this.advisoryLearningProfile?.priorityKeys ?? []),
|
|
4368
|
+
...freshState.priorityKeys,
|
|
4369
|
+
]))),
|
|
4370
|
+
skipContextFilter: forceInject,
|
|
4371
|
+
recoveryKeys: postCompactionRecoveryKeys.length > 0 ? postCompactionRecoveryKeys : undefined,
|
|
4372
|
+
ledgerContext: input.ledgerContext,
|
|
4373
|
+
});
|
|
4374
|
+
// Merge new facts that weren't already in augmentedObservedFacts.
|
|
4375
|
+
// Uses entityKey as the dedup key, same as the mid-turn
|
|
4376
|
+
// filter below. A net-new fact count of 0 means the retry
|
|
4377
|
+
// resolved the same facts we already had — treat as empty.
|
|
4378
|
+
const existingKeys = new Set(augmentedObservedFacts.map((f) => f.entityKey));
|
|
4379
|
+
const netNewFacts = subTurnObserved.facts.filter((f) => !existingKeys.has(f.entityKey));
|
|
4380
|
+
if (netNewFacts.length > 0) {
|
|
4381
|
+
augmentedObservedFacts = [...augmentedObservedFacts, ...netNewFacts];
|
|
4382
|
+
augmentedObservedTotalFound = augmentedObservedTotalFound + netNewFacts.length;
|
|
4383
|
+
subTurnLoopPlan = {
|
|
4384
|
+
...subTurnLoopPlan,
|
|
4385
|
+
outcome: 'attempted_added',
|
|
4386
|
+
addedFactCount: netNewFacts.length,
|
|
4387
|
+
note: `Sub-turn loop added ${netNewFacts.length} fact(s) via proposed hints (${subTurnLoopPlan.proposedHints.join(', ')}).`,
|
|
4388
|
+
};
|
|
4389
|
+
}
|
|
4390
|
+
else {
|
|
4391
|
+
subTurnLoopPlan = {
|
|
4392
|
+
...subTurnLoopPlan,
|
|
4393
|
+
outcome: 'attempted_empty',
|
|
4394
|
+
addedFactCount: 0,
|
|
4395
|
+
note: `Sub-turn loop with proposed hints (${subTurnLoopPlan.proposedHints.join(', ')}) returned no net-new facts.`,
|
|
4396
|
+
};
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
catch (err) {
|
|
4400
|
+
// Sub-turn loop must never crash attend. Record a declined
|
|
4401
|
+
// outcome with the error and continue.
|
|
4402
|
+
subTurnLoopPlan = {
|
|
4403
|
+
...subTurnLoopPlan,
|
|
4404
|
+
outcome: 'attempted_empty',
|
|
4405
|
+
attempted: false,
|
|
4406
|
+
addedFactCount: 0,
|
|
4407
|
+
reason: `sub_turn_retry_threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
4408
|
+
note: 'Sub-turn loop aborted due to retry error; baseline retrieval stands.',
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4140
4412
|
// A5: tool-using Attendant (planned). Derive the proposed follow-up
|
|
4141
4413
|
// tool calls from current brief state + retrieval outcome + drift.
|
|
4142
4414
|
// Pure and deterministic — the Attendant does not execute them.
|
|
@@ -4368,6 +4640,13 @@ If no durable facts can be extracted, return an empty array: [].`,
|
|
|
4368
4640
|
// the Attendant would consult if council mode were live.
|
|
4369
4641
|
// Pure, deterministic, never executed here.
|
|
4370
4642
|
councilConsultationPlan,
|
|
4643
|
+
// B8 M6: sub-turn reasoning loop plan — when a mid-turn attend
|
|
4644
|
+
// carries a partial assistant response, the Attendant re-scores
|
|
4645
|
+
// that partial against baseline retrieval and fires at most one
|
|
4646
|
+
// extra observe() call with broadened hints harvested from the
|
|
4647
|
+
// partial text. attempted=false / decline outcomes are echoed
|
|
4648
|
+
// on non-mid-turn phases so hosts can safely destructure.
|
|
4649
|
+
subTurnLoopPlan,
|
|
4371
4650
|
};
|
|
4372
4651
|
if (input.suppressEvents !== true) {
|
|
4373
4652
|
(0, staffEventRegistry_1.getStaffEventEmitter)().emit({
|